From 70b536816df3cad7280fdda4aff491204d2ee5fc Mon Sep 17 00:00:00 2001 From: rektdeckard Date: Thu, 13 Aug 2026 16:40:59 -0600 Subject: [PATCH 1/4] feat(agents): implement cross-region deployment confirmation --- cmd/lk/agent.go | 79 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/cmd/lk/agent.go b/cmd/lk/agent.go index fbca6857..55d6918f 100644 --- a/cmd/lk/agent.go +++ b/cmd/lk/agent.go @@ -1938,9 +1938,14 @@ func getClientSettings(ctx context.Context) (map[string]string, error) { // picker populated from server-reported available_regions when --region is // unset and the CLI is interactive. In non-interactive mode an unset --region // is an error so invocations fail loudly instead of silently defaulting. +// +// Regions the server flags in residency_warning_regions are annotated in the +// picker and confirmed before use; see confirmRegionResidency. func resolveRegion(cmd *cli.Command, settingsMap map[string]string, title string) (string, error) { + warnRegions := splitSetting(settingsMap["residency_warning_regions"]) + if region := cmd.String("region"); region != "" { - return region, nil + return region, confirmRegionResidency(cmd, region, settingsMap["project_data_region"], warnRegions) } availableRegionsStr, ok := settingsMap["available_regions"] @@ -1950,10 +1955,7 @@ func resolveRegion(cmd *cli.Command, settingsMap map[string]string, title string return "us-east", nil } - regionOptions := strings.Split(availableRegionsStr, ",") - for i, r := range regionOptions { - regionOptions[i] = strings.TrimSpace(r) - } + regionOptions := splitSetting(availableRegionsStr) slices.Sort(regionOptions) slices.Reverse(regionOptions) @@ -1961,19 +1963,84 @@ func resolveRegion(cmd *cli.Command, settingsMap map[string]string, title string return "", fmt.Errorf("non-interactive mode: --region flag must be specified, available regions: %v", regionOptions) } + options := make([]huh.Option[string], 0, len(regionOptions)) + for _, r := range regionOptions { + label := r + if slices.Contains(warnRegions, r) { + label = r + " " + util.Dimmed("(WARNING: data residency)") + } + options = append(options, huh.NewOption(label, r)) + } + var region string if err := huh.NewSelect[string](). Title(title). - Options(huh.NewOptions(regionOptions...)...). + Options(options...). Value(®ion). WithTheme(util.Theme). Run(); err != nil { return "", err } + if err := confirmRegionResidency(cmd, region, settingsMap["project_data_region"], warnRegions); err != nil { + return "", err + } out.Statusf("Using region [%s]", util.Accented(region)) return region, nil } +// confirmRegionResidency asks the user to confirm deploying into a region the +// server flagged as a data-residency risk for this project — an EU region while +// the project stores its data elsewhere, meaning an agent that may serve EU end +// users records their data outside the EU. +// +// Deploying there is permitted, so this only prompts; in non-interactive mode it +// warns and proceeds rather than failing, since the region was named explicitly +// and blocking would break existing automation. An older server sends no warning +// regions, in which case nothing here fires. +func confirmRegionResidency(cmd *cli.Command, region, dataRegion string, warnRegions []string) error { + if !slices.Contains(warnRegions, region) { + return nil + } + + detail := fmt.Sprintf( + "Your project data region is [%s]. Customer data may be stored outside of [eu].", + dataRegion, + ) + if SkipPrompts(cmd) { + out.Warnf("Deploying to [%s]. %s", region, detail) + return nil + } + + confirmed := false + if err := huh.NewForm(huh.NewGroup(util.Confirm(). + Title(fmt.Sprintf("Are you sure you want to deploy to %s?", util.Accented(region))). + Description(detail). + Affirmative("Deploy"). + Negative("Cancel"). + Value(&confirmed))). + WithTheme(util.Theme). + Run(); err != nil { + return err + } + if !confirmed { + return fmt.Errorf("deployment to %s cancelled", region) + } + return nil +} + +// splitSetting parses a comma-separated client setting into trimmed values, +// returning nil for an absent or empty setting. +func splitSetting(value string) []string { + if strings.TrimSpace(value) == "" { + return nil + } + parts := strings.Split(value, ",") + for i, p := range parts { + parts[i] = strings.TrimSpace(p) + } + return parts +} + func requireConfig(workingDir, tomlFilename string) (bool, error) { if lkConfig != nil { return true, nil From f8673e0e07c48d885a882f842678431717d04f34 Mon Sep 17 00:00:00 2001 From: rektdeckard Date: Thu, 13 Aug 2026 18:03:36 -0600 Subject: [PATCH 2/4] chore(agents): update gdpr warning copy --- cmd/lk/agent.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/lk/agent.go b/cmd/lk/agent.go index 55d6918f..becbddd8 100644 --- a/cmd/lk/agent.go +++ b/cmd/lk/agent.go @@ -1967,7 +1967,7 @@ func resolveRegion(cmd *cli.Command, settingsMap map[string]string, title string for _, r := range regionOptions { label := r if slices.Contains(warnRegions, r) { - label = r + " " + util.Dimmed("(WARNING: data residency)") + label = r + " " + util.Dimmed("⚠︎ GDPR compliance required") } options = append(options, huh.NewOption(label, r)) } @@ -2003,7 +2003,8 @@ func confirmRegionResidency(cmd *cli.Command, region, dataRegion string, warnReg } detail := fmt.Sprintf( - "Your project data region is [%s]. Customer data may be stored outside of [eu].", + "Data residency warning: This agent will run in [%s], but your project observability region is [%s]. Its recordings, transcripts, and traces will be stored outside the EU, which may breach GDPR. To keep this data in-region, create a new project with with an EU observability region.", + region, dataRegion, ) if SkipPrompts(cmd) { @@ -2013,7 +2014,7 @@ func confirmRegionResidency(cmd *cli.Command, region, dataRegion string, warnReg confirmed := false if err := huh.NewForm(huh.NewGroup(util.Confirm(). - Title(fmt.Sprintf("Are you sure you want to deploy to %s?", util.Accented(region))). + Title(fmt.Sprintf("Are you sure you want to deploy to [%s]?", region)). Description(detail). Affirmative("Deploy"). Negative("Cancel"). @@ -2023,7 +2024,7 @@ func confirmRegionResidency(cmd *cli.Command, region, dataRegion string, warnReg return err } if !confirmed { - return fmt.Errorf("deployment to %s cancelled", region) + return fmt.Errorf("deployment cancelled") } return nil } @@ -2065,7 +2066,6 @@ func generateAgentDockerfile(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } - projectType, err := agentfs.DetectProjectType(os.DirFS(workingDir)) if err != nil { return noAgentError() From d33842c4f7ddd91427eb0643869c8e82d415f342 Mon Sep 17 00:00:00 2001 From: rektdeckard Date: Fri, 14 Aug 2026 13:57:22 -0600 Subject: [PATCH 3/4] chore(test): add backwards-compatibility tests for region warning --- cmd/lk/agent.go | 2 +- cmd/lk/agent_test.go | 28 ++++++++++++++++++++++++++++ pkg/util/theme.go | 10 ++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/cmd/lk/agent.go b/cmd/lk/agent.go index becbddd8..861774ba 100644 --- a/cmd/lk/agent.go +++ b/cmd/lk/agent.go @@ -1967,7 +1967,7 @@ func resolveRegion(cmd *cli.Command, settingsMap map[string]string, title string for _, r := range regionOptions { label := r if slices.Contains(warnRegions, r) { - label = r + " " + util.Dimmed("⚠︎ GDPR compliance required") + label = r + " " + util.Warn("⚠︎ GDPR compliance required") } options = append(options, huh.NewOption(label, r)) } diff --git a/cmd/lk/agent_test.go b/cmd/lk/agent_test.go index 1bfe40cd..373e890f 100644 --- a/cmd/lk/agent_test.go +++ b/cmd/lk/agent_test.go @@ -602,3 +602,31 @@ func TestResolveAttributes(t *testing.T) { }) } } + +// An older server sends neither residency_warning_regions nor project_data_region. +// The advisory must then be inert rather than erroring or prompting, so a new CLI +// keeps working against a server that predates it. +func TestConfirmRegionResidencyWithoutServerSettings(t *testing.T) { + settingsMap := map[string]string{"available_regions": "us-east,eu-central"} + warnRegions := splitSetting(settingsMap["residency_warning_regions"]) + require.Empty(t, warnRegions) + + err := confirmRegionResidency(&cli.Command{}, "eu-central", + settingsMap["project_data_region"], warnRegions) + require.NoError(t, err) +} + +// An EU project gets the param with an empty value; it must behave like absent. +func TestConfirmRegionResidencyWithEmptyWarningRegions(t *testing.T) { + warnRegions := splitSetting("") + require.Empty(t, warnRegions) + require.NoError(t, confirmRegionResidency(&cli.Command{}, "eu-central", "eu", warnRegions)) +} + +// A flagged region without a TTY (or with --yes) warns and proceeds rather than +// blocking, so existing automation deploying into the EU keeps working. +func TestConfirmRegionResidencyProceedsWhenPromptsSkipped(t *testing.T) { + warnRegions := splitSetting("eu-central") + require.Equal(t, []string{"eu-central"}, warnRegions) + require.NoError(t, confirmRegionResidency(&cli.Command{}, "eu-central", "us", warnRegions)) +} diff --git a/pkg/util/theme.go b/pkg/util/theme.go index 96d7d434..e45008f0 100644 --- a/pkg/util/theme.go +++ b/pkg/util/theme.go @@ -177,6 +177,16 @@ func Dimmed(text string) string { return Theme.Focused.Description.Render(text) } +// Warn renders text in the active theme's Warning style +func Warn(text string) string { + return lipgloss.NewStyle().Foreground(activePalette.Warning).Render(text) +} + +// Warn renders text in the active theme's Error style +func Err(text string) string { + return lipgloss.NewStyle().Foreground(activePalette.Error).Render(text) +} + // Hyperlink wraps label in an OSC 8 terminal hyperlink pointing at url. Terminals // that support OSC 8 render label as a clickable link; others ignore the escape // and show label unchanged. Gate calls on an interactive terminal (see From 23a43b341b7aee3697f4b0493359ef7f9e503e54 Mon Sep 17 00:00:00 2001 From: rektdeckard Date: Fri, 14 Aug 2026 14:01:13 -0600 Subject: [PATCH 4/4] chore: bump patch version --- version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.go b/version.go index 469ccdb2..2da43e98 100644 --- a/version.go +++ b/version.go @@ -15,5 +15,5 @@ package livekitcli const ( - Version = "2.18.2" + Version = "2.18.3" )