Skip to content

Commit c24a0fd

Browse files
committed
feat(config): session correlation header injection configuration
Add YAML and CLI configuration surface for session correlation header injection per the Bridge/Boundaries Correlation RFC (FR 2). New configuration options: - --enable-session-correlation / session_correlation_enabled: top-level toggle to disable injection entirely for deployments without AI Bridge in front. - --inject-session-id-on / session_id_inject_targets (YAML): repeatable list of inject targets in "domain=<host> [path=<glob>]" format. - --session-id-header-name / session_id_header_name: configurable header name (default X-Coder-Agent-Firewall-Session-Id). - --sequence-number-header-name / sequence_number_header_name: configurable header name (default X-Coder-Agent-Firewall-Sequence-Number). Config validation ensures that when correlation is enabled at least one inject target is present and header names are non-empty. Parsing validates the domain=... path=... key-value format and rejects unknown keys. This commit adds config and validation only; runtime injection is wired in a follow-up PR.
1 parent 57c9456 commit c24a0fd

4 files changed

Lines changed: 514 additions & 0 deletions

File tree

cli/cli.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,43 @@ func BaseCommand(version string) *serpent.Command {
169169
Value: &showVersion,
170170
YAML: "", // CLI only
171171
},
172+
// Session correlation header injection options.
173+
{
174+
Flag: "enable-session-correlation",
175+
Env: "BOUNDARY_SESSION_CORRELATION_ENABLED",
176+
Description: "Enable session correlation header injection. Disable for deployments without AI Bridge in front.",
177+
Value: &cliConfig.SessionCorrelationEnabled,
178+
YAML: "session_correlation_enabled",
179+
},
180+
{
181+
Flag: "inject-session-id-on",
182+
Env: "BOUNDARY_INJECT_SESSION_ID_ON",
183+
Description: `Inject target (repeatable). Requests matching these targets receive session correlation headers. Format: "domain=<host> [path=<glob>]".`,
184+
Value: &cliConfig.InjectSessionIDOn,
185+
YAML: "", // CLI only, YAML uses session_id_inject_targets.
186+
},
187+
{
188+
Flag: "", // No CLI flag, YAML only.
189+
Description: "Inject targets from config file (YAML only).",
190+
Value: &cliConfig.InjectSessionIDOnYAML,
191+
YAML: "session_id_inject_targets",
192+
},
193+
{
194+
Flag: "session-id-header-name",
195+
Env: "BOUNDARY_SESSION_ID_HEADER_NAME",
196+
Description: "HTTP header name for the boundary session ID.",
197+
Default: config.DefaultSessionIDHeaderName,
198+
Value: &cliConfig.SessionIDHeaderName,
199+
YAML: "session_id_header_name",
200+
},
201+
{
202+
Flag: "sequence-number-header-name",
203+
Env: "BOUNDARY_SEQUENCE_NUMBER_HEADER_NAME",
204+
Description: "HTTP header name for the boundary sequence number.",
205+
Default: config.DefaultSequenceNumberHeaderName,
206+
Value: &cliConfig.SequenceNumberHeaderName,
207+
YAML: "sequence_number_header_name",
208+
},
172209
},
173210
Handler: func(inv *serpent.Invocation) error {
174211
// Handle --version flag early

config/config.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ type CliConfig struct {
6969
NoUserNamespace serpent.Bool `yaml:"no_user_namespace"`
7070
DisableAuditLogs serpent.Bool `yaml:"disable_audit_logs"`
7171
LogProxySocketPath serpent.String `yaml:"log_proxy_socket_path"`
72+
73+
// Session correlation header injection.
74+
SessionCorrelationEnabled serpent.Bool `yaml:"session_correlation_enabled"`
75+
InjectSessionIDOn AllowStringsArray `yaml:"inject_session_id_on"`
76+
InjectSessionIDOnYAML serpent.StringArray `yaml:"session_id_inject_targets"`
77+
SessionIDHeaderName serpent.String `yaml:"session_id_header_name"`
78+
SequenceNumberHeaderName serpent.String `yaml:"sequence_number_header_name"`
7279
}
7380

7481
type AppConfig struct {
@@ -86,6 +93,10 @@ type AppConfig struct {
8693
DisableAuditLogs bool
8794
LogProxySocketPath string
8895

96+
// SessionCorrelation controls header injection for AI Bridge
97+
// correlation. See SessionCorrelationConfig for details.
98+
SessionCorrelation SessionCorrelationConfig
99+
89100
// SessionID is a UUIDv4 generated at process startup. It groups
90101
// all audit events produced by this boundary invocation into a
91102
// single session. Set by Run, not by configuration.
@@ -107,6 +118,12 @@ func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string) (AppConfig, er
107118

108119
userInfo := GetUserInfo()
109120

121+
// Build session correlation config from CLI and YAML sources.
122+
sc, err := buildSessionCorrelation(cfg)
123+
if err != nil {
124+
return AppConfig{}, fmt.Errorf("session correlation config: %w", err)
125+
}
126+
110127
return AppConfig{
111128
AllowRules: allAllowStrings,
112129
LogLevel: cfg.LogLevel.Value(),
@@ -121,5 +138,46 @@ func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string) (AppConfig, er
121138
UserInfo: userInfo,
122139
DisableAuditLogs: cfg.DisableAuditLogs.Value(),
123140
LogProxySocketPath: cfg.LogProxySocketPath.Value(),
141+
SessionCorrelation: sc,
124142
}, nil
125143
}
144+
145+
// buildSessionCorrelation merges CLI and YAML inject target sources,
146+
// parses each target string, applies header name defaults, and
147+
// validates the resulting configuration.
148+
func buildSessionCorrelation(cfg CliConfig) (SessionCorrelationConfig, error) {
149+
// Merge YAML targets with CLI targets.
150+
rawTargets := append(cfg.InjectSessionIDOnYAML.Value(), cfg.InjectSessionIDOn.Value()...)
151+
152+
var targets []InjectTarget
153+
for _, raw := range rawTargets {
154+
t, err := ParseInjectTarget(raw)
155+
if err != nil {
156+
return SessionCorrelationConfig{}, err
157+
}
158+
targets = append(targets, t)
159+
}
160+
161+
// Apply defaults for header names.
162+
sessionIDHeader := cfg.SessionIDHeaderName.Value()
163+
if sessionIDHeader == "" {
164+
sessionIDHeader = DefaultSessionIDHeaderName
165+
}
166+
seqHeader := cfg.SequenceNumberHeaderName.Value()
167+
if seqHeader == "" {
168+
seqHeader = DefaultSequenceNumberHeaderName
169+
}
170+
171+
sc := SessionCorrelationConfig{
172+
Enabled: cfg.SessionCorrelationEnabled.Value(),
173+
InjectTargets: targets,
174+
SessionIDHeaderName: sessionIDHeader,
175+
SequenceNumberHeaderName: seqHeader,
176+
}
177+
178+
if err := ValidateSessionCorrelation(sc); err != nil {
179+
return SessionCorrelationConfig{}, err
180+
}
181+
182+
return sc, nil
183+
}

config/session_correlation.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// Default header names for session correlation.
9+
const (
10+
DefaultSessionIDHeaderName = "X-Coder-Agent-Firewall-Session-Id"
11+
DefaultSequenceNumberHeaderName = "X-Coder-Agent-Firewall-Sequence-Number"
12+
)
13+
14+
// InjectTarget represents a parsed target for session correlation header
15+
// injection. Requests matching the domain (and optional path glob) will
16+
// receive the session ID and sequence number headers.
17+
type InjectTarget struct {
18+
Domain string
19+
Path string
20+
}
21+
22+
// SessionCorrelationConfig holds configuration for session correlation
23+
// header injection. When enabled, boundary injects its session ID and
24+
// sequence number as custom headers on matching outbound requests so
25+
// that an upstream AI Bridge can correlate the request back to the
26+
// boundary audit event stream.
27+
type SessionCorrelationConfig struct {
28+
// Enabled controls whether session correlation headers are injected.
29+
// Deployments without AI Bridge in front should set this to false.
30+
Enabled bool
31+
32+
// InjectTargets is the list of domain/path patterns that should
33+
// receive session correlation headers.
34+
InjectTargets []InjectTarget
35+
36+
// SessionIDHeaderName is the HTTP header name used to carry the
37+
// boundary session ID. Defaults to DefaultSessionIDHeaderName.
38+
SessionIDHeaderName string
39+
40+
// SequenceNumberHeaderName is the HTTP header name used to carry
41+
// the boundary sequence number. Defaults to
42+
// DefaultSequenceNumberHeaderName.
43+
SequenceNumberHeaderName string
44+
}
45+
46+
// ParseInjectTarget parses a string of the form "domain=... path=..."
47+
// into an InjectTarget. The domain key is required; path is optional.
48+
func ParseInjectTarget(raw string) (InjectTarget, error) {
49+
raw = strings.TrimSpace(raw)
50+
if raw == "" {
51+
return InjectTarget{}, fmt.Errorf("inject target must not be empty")
52+
}
53+
54+
var target InjectTarget
55+
for _, part := range strings.Fields(raw) {
56+
key, value, ok := strings.Cut(part, "=")
57+
if !ok {
58+
return InjectTarget{}, fmt.Errorf(
59+
"inject target: malformed key-value pair %q, expected key=value", part,
60+
)
61+
}
62+
switch key {
63+
case "domain":
64+
if value == "" {
65+
return InjectTarget{}, fmt.Errorf("inject target: domain must not be empty")
66+
}
67+
target.Domain = value
68+
case "path":
69+
target.Path = value
70+
default:
71+
return InjectTarget{}, fmt.Errorf("inject target: unknown key %q", key)
72+
}
73+
}
74+
75+
if target.Domain == "" {
76+
return InjectTarget{}, fmt.Errorf("inject target: domain is required")
77+
}
78+
79+
return target, nil
80+
}
81+
82+
// ValidateSessionCorrelation checks that the session correlation config
83+
// is internally consistent. It returns an error describing the first
84+
// problem found, or nil if the config is valid.
85+
func ValidateSessionCorrelation(cfg SessionCorrelationConfig) error {
86+
if !cfg.Enabled {
87+
return nil
88+
}
89+
90+
if len(cfg.InjectTargets) == 0 {
91+
return fmt.Errorf(
92+
"session correlation is enabled but no inject targets are configured",
93+
)
94+
}
95+
96+
if cfg.SessionIDHeaderName == "" {
97+
return fmt.Errorf("session-id-header-name must not be empty when session correlation is enabled")
98+
}
99+
100+
if cfg.SequenceNumberHeaderName == "" {
101+
return fmt.Errorf("sequence-number-header-name must not be empty when session correlation is enabled")
102+
}
103+
104+
return nil
105+
}

0 commit comments

Comments
 (0)