From 4f864099d63e019a284b42237a5481a2785e0cf2 Mon Sep 17 00:00:00 2001 From: Anthony Torns II Date: Mon, 19 Oct 2026 23:38:15 -0700 Subject: [PATCH] feat(setup): read the setup agent config through the gonfalon relay The setup agent now gets its instructions and model from the ldcli-setup-agent AI Config on catfood. The CLI evaluates the config locally with the Go AI SDK, with the detected facts as context attributes, so targeting can give each kind of project its own instructions and model. When the config loads, its instructions decide the task, and the first message gives only the facts from setup. No SDK key is on the user's machine. The SDK gets the placeholder key ldcli-placeholder, and a loopback proxy adds the user's access token in LD-CLI-Token and forwards each request to the relay at /internal/ldcli-setup-agent/sdk under the user's base URI. The relay adds the real key. The proxy accepts requests only under a random path. The SDK client polls once, because setup reads the config once per run. A quick check before the SDK starts makes a refusal fall back at once, and it reads the environment ID from the relay. The CLI sets every SDK option in code, because the SDK reads LD_ variables that ldcli also uses, and it turns the SDK's logging off. If the user is not logged in, or the config cannot load, setup uses the built-in instructions. Co-Authored-By: Claude Opus 5.5 --- cmd/setup/setup.go | 31 ++- go.mod | 85 +++--- go.sum | 188 ++++++++----- internal/setup/agent/agent.go | 3 + internal/setup/agent/agent_test.go | 11 + internal/setup/agent/aiconfig.go | 341 +++++++++++++++++++++++ internal/setup/agent/aiconfig_test.go | 202 ++++++++++++++ internal/setup/agent/opencode.go | 6 +- internal/setup/agent/prompt.go | 23 ++ internal/setup/agent/relay_proxy.go | 107 +++++++ internal/setup/agent/relay_proxy_test.go | 100 +++++++ internal/setup/agent/relay_sdk_test.go | 114 ++++++++ 12 files changed, 1103 insertions(+), 108 deletions(-) create mode 100644 internal/setup/agent/aiconfig.go create mode 100644 internal/setup/agent/aiconfig_test.go create mode 100644 internal/setup/agent/relay_proxy.go create mode 100644 internal/setup/agent/relay_proxy_test.go create mode 100644 internal/setup/agent/relay_sdk_test.go diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 8fe4ae05..91b34d06 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -1,7 +1,9 @@ package setup import ( + "context" "fmt" + "time" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -51,14 +53,39 @@ initialization code if OpenCode is installed, and the built-in templates if not. viper.GetBool(cliflags.AnalyticsOptOut), ).SendCommandRunEvent(cmdAnalytics.CmdRunEventProperties(cmd, "setup", nil)) }, - RunE: runSetupWizard(analyticsTrackerFn, svc, runner), + RunE: withConfigSource(runner, runSetupWizard(analyticsTrackerFn, svc, runner)), } cmd.Flags().String(agentFlag, "auto", "Use the setup agent to add initialization code: auto, on, or off") cmd.AddCommand(newDetectCmd(svc)) cmd.AddCommand(newInstallCmd(svc)) cmd.AddCommand(newInitCmd(svc)) - cmd.AddCommand(newAgentCmd(svc, runner)) + agentCmd := newAgentCmd(svc, runner) + agentCmd.RunE = withConfigSource(runner, agentCmd.RunE) + cmd.AddCommand(agentCmd) return cmd } + +// withConfigSource gives the agent the setup-agent AI Config for the +// logged-in user, and closes it after the command runs, so the AI SDK sends +// its events before ldcli exits. The source is built when the command runs, +// because the access token and the base URI come from flags and the +// configuration file. +func withConfigSource(runner *agent.OpenCode, run func(*cobra.Command, []string) error) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + source := agent.NewAIConfigSource( + viper.GetString(cliflags.BaseURIFlag), + viper.GetString(cliflags.AccessTokenFlag), + ) + runner.Config = source + if closer, ok := source.(interface{ Close(context.Context) error }); ok { + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = closer.Close(ctx) + }() + } + return run(cmd, args) + } +} diff --git a/go.mod b/go.mod index de5338b0..cd695c6d 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,16 @@ module github.com/launchdarkly/ldcli -go 1.26 +go 1.26.0 require ( github.com/adrg/frontmatter v0.2.0 github.com/adrg/xdg v0.5.3 github.com/atotto/clipboard v0.1.4 - github.com/aws/aws-sdk-go-v2 v1.47.0 - github.com/aws/aws-sdk-go-v2/config v1.33.5 + github.com/aws/aws-sdk-go-v2 v1.47.3 + github.com/aws/aws-sdk-go-v2/config v1.33.9 github.com/aws/aws-sdk-go-v2/service/devopsagent v1.17.0 github.com/aws/aws-sdk-go-v2/service/iam v1.64.0 - github.com/aws/aws-sdk-go-v2/service/sts v1.51.0 + github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 github.com/blacktop/go-dwarf v1.0.14 github.com/blacktop/go-macho v1.1.282 github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 @@ -27,6 +27,7 @@ require ( github.com/iancoleman/strcase v0.3.0 github.com/ianlancetaylor/demangle v0.0.0-20260505044615-1ff4bf46051f github.com/launchdarkly/api-client-go/v24 v24.0.0 + github.com/launchdarkly/go-ai-sdk v0.0.0-20261016194641-6152dc96d702 github.com/launchdarkly/go-sdk-common/v3 v3.6.0 github.com/launchdarkly/go-server-sdk/v7 v7.19.1 github.com/launchdarkly/sdk-meta/api v0.7.5 @@ -34,19 +35,19 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/muesli/reflow v0.3.0 github.com/oapi-codegen/oapi-codegen/v2 v2.7.1 - github.com/oapi-codegen/runtime v1.1.2 - github.com/pelletier/go-toml/v2 v2.4.3 + github.com/oapi-codegen/runtime v1.6.0 + github.com/pelletier/go-toml/v2 v2.2.4 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 - github.com/samber/lo v1.53.0 + github.com/samber/lo v1.51.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.1 go.uber.org/mock v0.5.2 golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa - golang.org/x/term v0.40.0 + golang.org/x/term v0.46.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -54,37 +55,40 @@ require ( github.com/BurntSushi/toml v1.3.2 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.20.5 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.3 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.10.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.38.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.0 // indirect - github.com/aws/smithy-go v1.28.1 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.20.9 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 // indirect + github.com/aws/smithy-go v1.28.5 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/catppuccin/go v0.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.11.0 // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-openapi/jsonpointer v0.22.5 // indirect - github.com/go-openapi/swag/jsonname v0.25.5 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v1.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gregjones/httpcache v0.0.0-20171119193500-2bcd89a1743f // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/launchdarkly/ccache v1.1.0 // indirect @@ -116,19 +120,32 @@ require ( github.com/speakeasy-api/openapi v1.19.2 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/objx v0.5.3 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.7.8 // indirect github.com/yuin/goldmark-emoji v1.0.5 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.50.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/tools v0.42.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 // indirect + go.opentelemetry.io/otel/log v1.47.0 // indirect + go.opentelemetry.io/otel/metric v1.47.0 // indirect + go.opentelemetry.io/otel/sdk v1.47.0 // indirect + go.opentelemetry.io/otel/trace v1.47.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/mod v0.41.0 // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + golang.org/x/tools v0.49.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect gopkg.in/validator.v2 v2.0.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 26ff2d63..58438c46 100644 --- a/go.sum +++ b/go.sum @@ -18,40 +18,40 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7D github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go-v2 v1.47.0 h1:0jsHallhJCeaU0Ko48c/3FK1ctOQ7NpzggxriJOQ8MQ= -github.com/aws/aws-sdk-go-v2 v1.47.0/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 h1:GPRlPwz40I2B2VrBEASOA3Bi77NyeqejNLkifosX0rs= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20/go.mod h1:g7PNzKcsOKWb4fkSRBA7BZVAS6Y8IcxzN+nRohhQ1Q8= -github.com/aws/aws-sdk-go-v2/config v1.33.5 h1:UA1dmokBFOLFoOyVBhO6HjM6edy0MIk5AZSkJVcksQw= -github.com/aws/aws-sdk-go-v2/config v1.33.5/go.mod h1:Dop8axzz0xx38GExIYWXdeyc8QQ7Cr+nPsxpD/LYy4U= -github.com/aws/aws-sdk-go-v2/credentials v1.20.5 h1:wklUVvHMc9xTQ3rcp49/ISpiMnhbCicJcA6n6S8m7J8= -github.com/aws/aws-sdk-go-v2/credentials v1.20.5/go.mod h1:fyEdrn6ccLFOkoK84j5bQyGTxp9zPt5l2XMhxf4DVZs= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.0 h1:AM4hHjww+PSFtt6E+UrBrPlZkWsePCLEt9AjkfQX+yM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.0/go.mod h1:3x/yXezeQjpOvBb4jEMxrS8SXvpdvJ5abv6l5c1gWM8= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.3 h1:Hp/VgjP0BysR3OgLlR057Vz2LcbbVnoWeJ+3qWiS/fY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.3/go.mod h1:nwGV5qw7F1IZPgxCvA/ph8N2TAuz+BkRG/bXn808qMA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.3 h1:MUaM4f+kj1ZIBPZfUS8cxP1GKXXZtHJjAthy93AN7SM= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.3/go.mod h1:6YmVmEVRI5ZZzRjCSsb9SryKH0hAlMRdgA7kG9aDvBU= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.3 h1:fuSCw4Z2qfRCztMPO3GXJNSiEp6Wee+WOLwrHHUMy9c= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.3/go.mod h1:6SxcHheD1pPR5+kWm1wGvjlL/YqUsh267sAfEmN4K7A= +github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk= +github.com/aws/aws-sdk-go-v2 v1.47.3/go.mod h1:xSl+OSI5ZUw6bSMupWc218zE5Pl+65MCCu3ySWFY99A= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 h1:oiEmPz/YMMeh/JvAE8xSxNCIsvWE6lpFe4ZPlyFykQI= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22/go.mod h1:QWUDuRi9PFjBHIU2iH1Pkldbe7zqLfXJC4wWLbF8nuQ= +github.com/aws/aws-sdk-go-v2/config v1.33.9 h1:Hv6bcQUJVnmuWaKjqD6Ot8sE75NrWsWV20oGT5aWle0= +github.com/aws/aws-sdk-go-v2/config v1.33.9/go.mod h1:ivowmrZNfBQLcg4QkaItgi5wae4Bzot9RzIdbY1hfTg= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9 h1:SNO55PnWhaDafsOcv16lCA0kdoFMl/YlegWoVsU02s0= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9/go.mod h1:+2reOTRtdD3VSBqpRDNwrZIYy8yMCYa7oK3WKTJ/I1I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 h1:gr3b87OLB29jymWPzp/CAyXwQRJVE+sC4Wm0qBIKHsk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3/go.mod h1:BRjf4L/Yx2hY54o+ZibPbLJyMMeSwqYQXwcJfqW66a8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 h1:CqL8NN3p4I0JTwwV5MEoatIl4Y2amZ2X9dRD+0hbRL0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6/go.mod h1:exdJ32hMqXl0Wyo6NwqEC5yIhtua3+PDaKE8P29m4qE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 h1:FpBq24TG6WVSObTNhQOv7Q0088rounUUlwn3zlNItkM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6/go.mod h1:ONzdPVYC95oBOu0VWNEsKEntcoemP3xVKjT+qWml1s4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 h1:Qmn60RWuk9+bd2ddvJJqwf3Cpac3VvGMOdbGemtUhX4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6/go.mod h1:h/YJhe4AVyQxEUMluph10iqV7mjajH7A9L+BHF5ppis= github.com/aws/aws-sdk-go-v2/service/devopsagent v1.17.0 h1:nVr3wL92fkSof1T+blrzUEKJzWa1Kpp9zBhTKG9Wa5c= github.com/aws/aws-sdk-go-v2/service/devopsagent v1.17.0/go.mod h1:gT39kJ1GWdG7BOdHmzqOTe3lIaX70nJPQUNxd+nWnXU= github.com/aws/aws-sdk-go-v2/service/iam v1.64.0 h1:CjgiPuyW26WQeQ934DxMNuwkDg6+ziqgPN9lHQ5rwYA= github.com/aws/aws-sdk-go-v2/service/iam v1.64.0/go.mod h1:TI7OT5XlpXfs4BM4Y5NXcHTiyBwkqv6NpGiM4/usn5A= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.3 h1:bON1rJf67TSTDCKg816AAIE4xSTtoo9tl0XRkO72R+I= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.3/go.mod h1:c5BBpjJcQXpfeq9iASyVKA3T6vX6B6LEXY4mL/gklDY= -github.com/aws/aws-sdk-go-v2/service/signin v1.10.0 h1:ZD5qFpWcaOKdTuhBi431pIDkCgrMkMlMT6jlpSPoIRI= -github.com/aws/aws-sdk-go-v2/service/signin v1.10.0/go.mod h1:8Nuuf+tR346PjJ3MvZPh9pekbLiLQFWJhzMXfwy7alA= -github.com/aws/aws-sdk-go-v2/service/sso v1.38.0 h1:JGeeBcMlhg1xtOXYpeCaTQBZObtXMPQCUqBcmr65NRA= -github.com/aws/aws-sdk-go-v2/service/sso v1.38.0/go.mod h1:XwteswG9EOMRFm73UT0t+MbTwyLxMrEXkU6e+v92Lzo= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.0 h1:obhahQXDEdVEv8y5bTKXR30LVaxYe1kyYM0L7l2Iq+k= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.0/go.mod h1:6twZZ/aXHNy1vXUO8koUbp++MYzMASkOgEBdkbJYmO0= -github.com/aws/aws-sdk-go-v2/service/sts v1.51.0 h1:Zpnqa6XtrNzXZnwbdCqHOXpXhMsa01ql/pcRQ1sb4hk= -github.com/aws/aws-sdk-go-v2/service/sts v1.51.0/go.mod h1:/8JRcdTt//hG0Q4BTmGbuOplT7ABe+5rdtqUHqXvYIM= -github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ= -github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 h1:e6YW5uME30TkrElR+4/xcY7NB6CCj2dcv7vtmGXhfhQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21/go.mod h1:obpHAMP693+f3/A8PkLhIqfK1C+NWWn8zXzqD3OH520= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 h1:LHHdobjo5le1sjJdolitV7uVvYg3wkaeM1Ju2fDgeZQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6/go.mod h1:CBGB0evAiaMD3Lk4wPO7LQCcCZLuhzL5Qf/wfEUHQ5w= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 h1:tvpDjuUvgKURf3Yk4hOaGbZ4QdY+070OWcA5d6LUv2A= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4/go.mod h1:3ibVKg5Ng82Zby8++jELTD2nHxiPk2DJRLEeW/LKp8M= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 h1:ChCtagixSFOxltv0o6E3NT8F7ksvZtZGPwUBynO5Nmk= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4/go.mod h1:+FEO9jrVMpc9oN3CdAxJbtlQEu3z8UddDPSedpeM3PY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 h1:j6c+HA4AoS0EBEjArPjavFEBrucJEryfrQU0TffmACk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4/go.mod h1:RhghaXUk9q66VtOQUX1FdPddQuHnqQZzbB0fEYXmPRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 h1:OKEfbf2tcfsF2pBk/S9QqNpo6alaAtiBs4hysCsBjPw= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4/go.mod h1:mCd6qGo9b8hTtfjgsmNkFlzwjyce4owd7HDVFnCLXhM= +github.com/aws/smithy-go v1.28.5 h1:OqbqKtzY9NUnr2yAe9+hiTdFb20tNaK/lI0PMOGXX9k= +github.com/aws/smithy-go v1.28.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= @@ -65,6 +65,10 @@ github.com/blacktop/go-macho v1.1.282/go.mod h1:Hc5E2Lvt/U1VT+jOxr1O5l/LNFJeMYK4 github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= @@ -105,8 +109,6 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 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= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= @@ -116,8 +118,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -126,15 +128,18 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/getkin/kin-openapi v0.144.0 h1:hIRcTH+KjLfkLpYU6bSSfdFpi0fZi1fp+hSPi4aQu9Y= github.com/getkin/kin-openapi v0.144.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY= -github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= -github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= -github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= -github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= -github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= -github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= +github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= +github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= +github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -144,12 +149,14 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -161,6 +168,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gregjones/httpcache v0.0.0-20171119193500-2bcd89a1743f h1:kOkUP6rcVVqC+KlKKENKtgfFfJyDySYhqL9srXooghY= github.com/gregjones/httpcache v0.0.0-20171119193500-2bcd89a1743f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -191,6 +200,8 @@ github.com/launchdarkly/ccache v1.1.0 h1:voD1M+ZJXR3MREOKtBwgTF9hYHl1jg+vFKS/+VA github.com/launchdarkly/ccache v1.1.0/go.mod h1:TlxzrlnzvYeXiLHmesMuvoZetu4Z97cV1SsdqqBJi1Q= github.com/launchdarkly/eventsource v1.14.0 h1:6lxVkUwxCAEbBrx8J/N9i25yOajC6NNwq0Voo+fT0Kc= github.com/launchdarkly/eventsource v1.14.0/go.mod h1:dU+rZxkPOlGPsyJPpiDqiepAcFwIITDUClY9+A6RrMw= +github.com/launchdarkly/go-ai-sdk v0.0.0-20261016194641-6152dc96d702 h1:HhCHqLB8Qvyd2YnATyaCwOsViAZwYTwSQhWHzEV2Dfg= +github.com/launchdarkly/go-ai-sdk v0.0.0-20261016194641-6152dc96d702/go.mod h1:l4YMlzJALpe2Y5Xjy/4r55r+IRw2uIvxckTWlGKqh6Y= github.com/launchdarkly/go-jsonstream/v3 v3.1.2 h1:Od0QEKHesbKnD3GgAt+LkZzhcUiSP+yhrclrhsoox6w= github.com/launchdarkly/go-jsonstream/v3 v3.1.2/go.mod h1:ZBjhKq8mhArCtqotGRGnteY6eXpNm1GaOdUSZHh+ZjM= github.com/launchdarkly/go-sdk-common/v3 v3.6.0 h1:DxDfcNcRMxN2E8X7aWfOLcVvHgD2iUgFRYCcZtVcK3w= @@ -237,10 +248,12 @@ github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= +github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= github.com/oapi-codegen/oapi-codegen/v2 v2.7.1 h1:a7Ab7YlpqkVG5HKrTaeFstm32Z5QOnyjnbsCO0jiMYM= github.com/oapi-codegen/oapi-codegen/v2 v2.7.1/go.mod h1:qzFy6iuobJw/hD1aRILee4G87/ShmhR0xYCwcUtZMCw= -github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= -github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/oapi-codegen/runtime v1.6.0 h1:7Xx+GlueD6nRuyKoCPzL434Jfi3BetbiJOrzCHp/VPU= +github.com/oapi-codegen/runtime v1.6.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY= github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU= github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw= @@ -260,8 +273,8 @@ github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= -github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -273,15 +286,15 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= -github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= +github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= @@ -305,14 +318,14 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= @@ -327,18 +340,41 @@ github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -346,13 +382,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -370,27 +406,35 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= +golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -399,6 +443,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/setup/agent/agent.go b/internal/setup/agent/agent.go index 7116c058..6fcaf103 100644 --- a/internal/setup/agent/agent.go +++ b/internal/setup/agent/agent.go @@ -32,6 +32,9 @@ type Config struct { ProviderID string ModelID string Instructions string + // Task is the first user message. If it is empty, the run sends the + // CLI's own task message from BuildPrompt. + Task string } // ConfigSource gives the agent configuration for a task. diff --git a/internal/setup/agent/agent_test.go b/internal/setup/agent/agent_test.go index ee423479..76f5e46f 100644 --- a/internal/setup/agent/agent_test.go +++ b/internal/setup/agent/agent_test.go @@ -42,6 +42,17 @@ func TestCredentialEnvVar(t *testing.T) { assert.Equal(t, MobileKeyEnvVar, CredentialEnvVar("swift-client-sdk")) } +func TestBuildFacts_GivesFactsAndNoTask(t *testing.T) { + facts := BuildFacts(Task{SDKID: "python-server-sdk", Language: "Python", EntryPoint: "main.py", FlagKey: "my-new-flag", SDKInstalled: true}) + + assert.Contains(t, facts, "- SDK: python-server-sdk") + assert.Contains(t, facts, "- Entry point: main.py") + assert.Contains(t, facts, "- SDK package installed: yes") + assert.Contains(t, facts, "- Flag created by setup: my-new-flag") + assert.Contains(t, facts, "- Environment variable for the SDK credential: LAUNCHDARKLY_SDK_KEY") + assert.NotContains(t, facts, "Evaluate") +} + func TestBuildPrompt_NoEntryPoint_AsksTheAgentToFindOne(t *testing.T) { prompt := BuildPrompt(Task{SDKID: "go-server-sdk", FlagKey: "f"}) diff --git a/internal/setup/agent/aiconfig.go b/internal/setup/agent/aiconfig.go new file mode 100644 index 00000000..12e2197f --- /dev/null +++ b/internal/setup/agent/aiconfig.go @@ -0,0 +1,341 @@ +package agent + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "os" + "strings" + "sync" + "time" + + "github.com/launchdarkly/go-ai-sdk/aiserver" + "github.com/launchdarkly/go-sdk-common/v3/ldcontext" + ld "github.com/launchdarkly/go-server-sdk/v7" + "github.com/launchdarkly/go-server-sdk/v7/interfaces" + "github.com/launchdarkly/go-server-sdk/v7/ldcomponents" +) + +// SetupAgentConfigKey is the key of the setup-agent AI Config. +const SetupAgentConfigKey = "ldcli-setup-agent" + +const ( + contextKind = "ldcli-setup" + initTimeout = 5 * time.Second + // environmentIDEnv is the only setting that the AI SDK reads from the + // environment and that InitOptions cannot set. + environmentIDEnv = "LD_ENVIRONMENT_ID" + // RelayEnvironmentIDHeader is the response header in which the relay sends + // the client-side ID of the environment that it forwards to. The AI SDK + // sends that ID with its events and spans, so AgentControl can link runs + // to the config's Monitoring tab. The ID is not a secret, and the relay + // knows it because it holds that environment's SDK key. Like + // RelayTokenHeader, the name does not start with X-LD-. + RelayEnvironmentIDHeader = "LD-Setup-Agent-Environment-ID" +) + +// AIConfigSource reads the setup-agent AI Config with the LaunchDarkly AI SDK. +// It evaluates the config locally with the detected facts as context +// attributes, so targeting can give each kind of project its own +// instructions and model. +// +// The SDK reaches the config through the relay in gonfalon, which holds the +// real SDK key. The SDK gets a placeholder key, and a loopback proxy adds the +// user's access token to each request. So no SDK key is on the user's +// machine. +// +// If the SDK cannot start, for example because the relay refuses the request, +// or the config is missing or off, it returns the configuration of Fallback. +// Setup must not fail because the config is unavailable. +type AIConfigSource struct { + Key string + // RelayBaseURI is the user's base URI. The relay is at RelayPath under it. + RelayBaseURI string + AccessToken string + Fallback ConfigSource + + // checkRelay, initClient, and inspect are fields so that tests can replace + // them. + checkRelay func(ctx context.Context, pollingBase string) (string, error) + initClient func(ctx context.Context, opts aiserver.InitOptions) error + inspect func(ctx context.Context, key string, c ldcontext.Context) aiserver.InspectResult + + once sync.Once + initErr error + proxy *relayProxy + // started reports that this source started the SDK's global client, so + // Close must stop it. + started bool +} + +// NewAIConfigSource returns an AIConfigSource for a logged-in user, and +// DefaultConfigSource if there is no access token. +func NewAIConfigSource(baseURI, accessToken string) ConfigSource { + if strings.TrimSpace(accessToken) == "" || strings.TrimSpace(baseURI) == "" { + return DefaultConfigSource{} + } + return &AIConfigSource{ + Key: SetupAgentConfigKey, + RelayBaseURI: baseURI, + AccessToken: accessToken, + Fallback: DefaultConfigSource{}, + } +} + +// relayOptions sets every connection option of the AI SDK to the loopback +// proxy. The SDK fills an empty option from an environment variable, such as +// LD_BASE_URI or OTEL_EXPORTER_OTLP_ENDPOINT. ldcli uses LD_BASE_URI for its own +// API address, so an empty option would connect the SDK to the wrong place. +// +// The paths under the proxy match the relay routes: the SDK adds its own +// paths, such as /all for the stream and /v1/traces for spans. +func relayOptions(proxyBase string) aiserver.InitOptions { + return aiserver.InitOptions{ + SDKKey: RelayPlaceholderKey, + BaseURI: proxyBase + "/polling", + StreamURI: proxyBase + "/stream", + EventsURI: proxyBase + "/events", + OTLPEndpoint: proxyBase + "/otel", + ServiceName: "ldcli", + Environment: "ldcli-setup", + WaitFor: initTimeout, + } +} + +func (s *AIConfigSource) start(ctx context.Context) error { + proxy, err := startRelayProxy(s.RelayBaseURI, s.AccessToken) + if err != nil { + return err + } + s.proxy = proxy + opts := relayOptions(proxy.baseURL) + check := s.checkRelay + if check == nil { + check = checkRelay + } + environmentID, err := check(ctx, opts.BaseURI) + if err != nil { + return err + } + setEnvironmentID(environmentID) + if s.initClient != nil { + return s.initClient(ctx, opts) + } + if err := initQuietClient(ctx, opts); err != nil { + return err + } + s.started = true + return nil +} + +// setEnvironmentID sets LD_ENVIRONMENT_ID for this process, and clears it when +// the relay sent no ID, so a value in the user's shell for their own app does +// not reach the SDK. OpenCode does not get the variable, because the CLI +// removes LD_ variables from OpenCode's environment. +func setEnvironmentID(id string) { + if id == "" { + _ = os.Unsetenv(environmentIDEnv) + return + } + _ = os.Setenv(environmentIDEnv, id) +} + +// initQuietClient starts the AI SDK with a LaunchDarkly client that does not +// log. The SDK's own client logs to the terminal, which breaks the wizard's +// full-screen view, and InitOptions cannot turn its logs off. +// +// The client polls and does not open the flag stream. Setup reads the config +// once per run, so one polling request is all it needs, and it does not +// depend on a long-lived connection through the relay and the load balancer. +func initQuietClient(ctx context.Context, opts aiserver.InitOptions) error { + aiserver.SetLogger(slog.New(slog.NewTextHandler(io.Discard, nil))) + client, err := ld.MakeCustomClient(opts.SDKKey, ld.Config{ + ServiceEndpoints: interfaces.ServiceEndpoints{ + Polling: opts.BaseURI, + Streaming: opts.StreamURI, + Events: opts.EventsURI, + }, + DataSource: ldcomponents.PollingDataSource(), + Logging: ldcomponents.NoLogging(), + DiagnosticOptOut: true, + }, opts.WaitFor) + if err != nil { + if client != nil { + _ = client.Close() + } + return err + } + if _, err := aiserver.InitWithClient(ctx, client, opts); err != nil { + _ = client.Close() + return err + } + return nil +} + +func (s *AIConfigSource) Resolve(ctx context.Context, task Task) (Config, error) { + s.once.Do(func() { + s.initErr = s.start(ctx) + if s.initErr != nil { + s.closeProxy(ctx) + } + }) + if s.initErr != nil { + return s.fallback(ctx, task) + } + + inspect := s.inspect + if inspect == nil { + inspect = aiserver.InspectConfig + } + key := s.Key + if key == "" { + key = SetupAgentConfigKey + } + res := inspect(ctx, key, setupContext(task)) + if !res.Enabled || res.Config == nil { + return s.fallback(ctx, task) + } + return configFromAI(res.Config, task), nil +} + +func (s *AIConfigSource) fallback(ctx context.Context, task Task) (Config, error) { + if s.Fallback == nil { + return DefaultConfigSource{}.Resolve(ctx, task) + } + return s.Fallback.Resolve(ctx, task) +} + +// Close flushes the SDK events through the relay, stops the SDK client, and +// then stops the proxy. +func (s *AIConfigSource) Close(ctx context.Context) error { + var err error + if s.started { + err = aiserver.Shutdown(ctx) + s.started = false + } + s.closeProxy(ctx) + return err +} + +func (s *AIConfigSource) closeProxy(ctx context.Context) { + if s.proxy != nil { + _ = s.proxy.close(ctx) + s.proxy = nil + } +} + +// setupContext is the evaluation context for one run. Its attributes are the +// detected facts that targeting rules can match. It has no file paths and no +// names from the project, because the context goes to LaunchDarkly in +// analytics events. +func setupContext(t Task) ldcontext.Context { + key, err := randomHex(16) + if err != nil { + key = "ldcli-setup" + } + return ldcontext.NewBuilder(key). + Kind(contextKind). + Anonymous(true). + SetString("sdkId", t.SDKID). + SetString("language", t.Language). + SetString("framework", t.Framework). + SetString("packageManager", t.PackageManager). + SetBool("hasEntryPoint", t.EntryPoint != ""). + SetBool("sdkInstalled", t.SDKInstalled). + Build() +} + +// openCodeProviders maps AI Config provider names to OpenCode provider IDs +// where the two are different. +var openCodeProviders = map[string]string{ + "bedrock": "amazon-bedrock", + "gemini": "google", +} + +// configFromAI turns an AI Config variation into an agent configuration. An +// agent-mode config has instructions. A completion-mode config has messages, +// and its system messages become the instructions. +// +// The config's instructions decide the task. The first user message gives +// only the facts from setup. If the variation has no instructions, the +// built-in instructions and the CLI's own task message apply. +func configFromAI(c *aiserver.AiConfig, task Task) Config { + cfg := Config{Instructions: c.Instructions, ModelID: c.Model.Name} + if cfg.Instructions == "" { + var system []string + for _, m := range c.Messages { + if m.Role == "system" && m.Text != "" { + system = append(system, m.Text) + } + } + cfg.Instructions = strings.Join(system, "\n\n") + } + if cfg.Instructions == "" { + cfg.Instructions = DefaultInstructions + } else { + cfg.Task = BuildFacts(task) + } + provider := strings.ToLower(strings.TrimSpace(c.Provider.Name)) + if mapped, ok := openCodeProviders[provider]; ok { + provider = mapped + } + cfg.ProviderID = provider + // OpenCode needs both values to pick a model. With only one, OpenCode + // uses its default model. + if cfg.ProviderID == "" || cfg.ModelID == "" { + cfg.ProviderID, cfg.ModelID = "", "" + } + return cfg +} + +// RelayError is a refusal from the relay, for example because the kill switch +// is off or the access token is not valid. +type RelayError struct { + Status int + Code string + Message string +} + +func (e *RelayError) Error() string { + if e.Message != "" { + return fmt.Sprintf("the setup agent service refused the request (%s): %s", e.Code, e.Message) + } + if e.Code != "" { + return fmt.Sprintf("the setup agent service refused the request (%s)", e.Code) + } + return fmt.Sprintf("the setup agent service returned HTTP %d", e.Status) +} + +// checkRelay makes one polling request through the proxy before the SDK +// starts. The Go server SDK retries a refused stream until its start timeout +// ends, so without this check a refusal would delay every setup run. It +// returns the environment ID that the relay sends, or an empty string. +func checkRelay(ctx context.Context, pollingBase string) (string, error) { + ctx, cancel := context.WithTimeout(ctx, initTimeout) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, pollingBase+"/sdk/latest-all", nil) + if err != nil { + return "", err + } + req.Header.Set("Authorization", RelayPlaceholderKey) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("could not reach the setup agent service: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode < 300 { + return strings.TrimSpace(resp.Header.Get(RelayEnvironmentIDHeader)), nil + } + refusal := &RelayError{Status: resp.StatusCode} + var body struct { + Code string `json:"code"` + Message string `json:"message"` + } + if json.NewDecoder(io.LimitReader(resp.Body, 4096)).Decode(&body) == nil { + refusal.Code, refusal.Message = body.Code, body.Message + } + return "", refusal +} diff --git a/internal/setup/agent/aiconfig_test.go b/internal/setup/agent/aiconfig_test.go new file mode 100644 index 00000000..d6c56a69 --- /dev/null +++ b/internal/setup/agent/aiconfig_test.go @@ -0,0 +1,202 @@ +package agent + +import ( + "context" + "errors" + "os" + "strings" + "testing" + + "github.com/launchdarkly/go-ai-sdk/aiserver" + "github.com/launchdarkly/go-sdk-common/v3/ldcontext" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRelayOptions_SetsEveryConnectionOption(t *testing.T) { + opts := relayOptions("http://127.0.0.1:5000/secret") + + assert.Equal(t, aiserver.InitOptions{ + SDKKey: RelayPlaceholderKey, + BaseURI: "http://127.0.0.1:5000/secret/polling", + StreamURI: "http://127.0.0.1:5000/secret/stream", + EventsURI: "http://127.0.0.1:5000/secret/events", + OTLPEndpoint: "http://127.0.0.1:5000/secret/otel", + ServiceName: "ldcli", + Environment: "ldcli-setup", + WaitFor: initTimeout, + }, opts, "an empty option would make the SDK read an LD_ variable") +} + +func TestNewAIConfigSource(t *testing.T) { + assert.Equal(t, DefaultConfigSource{}, NewAIConfigSource("https://app.launchdarkly.com", ""), "no login, no AI Config") + + source, ok := NewAIConfigSource("https://app.launchdarkly.com", "api-123").(*AIConfigSource) + require.True(t, ok) + assert.Equal(t, SetupAgentConfigKey, source.Key) + assert.Equal(t, "https://app.launchdarkly.com", source.RelayBaseURI) + assert.Equal(t, "api-123", source.AccessToken) +} + +func TestAIConfigSource_StartsTheSDKBehindTheProxy(t *testing.T) { + t.Setenv(environmentIDEnv, "the-users-own-app") + var got aiserver.InitOptions + s := &AIConfigSource{ + RelayBaseURI: "https://app.launchdarkly.com", + AccessToken: "api-123", + checkRelay: func(context.Context, string) (string, error) { return "catfood-client-id", nil }, + initClient: func(_ context.Context, opts aiserver.InitOptions) error { + got = opts + return nil + }, + inspect: func(context.Context, string, ldcontext.Context) aiserver.InspectResult { + return aiserver.InspectResult{} + }, + } + defer s.Close(context.Background()) + + _, err := s.Resolve(context.Background(), Task{}) + + require.NoError(t, err) + assert.Equal(t, RelayPlaceholderKey, got.SDKKey) + assert.True(t, strings.HasPrefix(got.StreamURI, "http://127.0.0.1:"), got.StreamURI) + assert.NotContains(t, got.StreamURI, "api-123", "the token is never in an SDK option") + assert.Equal(t, "catfood-client-id", os.Getenv(environmentIDEnv), "the ID comes from the relay") +} + +func TestAIConfigSource_NoEnvironmentID_ClearsTheUsersValue(t *testing.T) { + t.Setenv(environmentIDEnv, "the-users-own-app") + s, _ := fakeSource(nil, aiserver.InspectResult{}) + defer s.Close(context.Background()) + + _, _ = s.Resolve(context.Background(), Task{}) + + _, set := os.LookupEnv(environmentIDEnv) + assert.False(t, set) +} + +type inspectCall struct { + key string + ctx ldcontext.Context +} + +func fakeSource(initErr error, res aiserver.InspectResult) (*AIConfigSource, *[]inspectCall) { + var calls []inspectCall + s := &AIConfigSource{ + Key: "setup", + RelayBaseURI: "https://app.launchdarkly.com", + AccessToken: "api-123", + checkRelay: noRelayCheck, + initClient: func(context.Context, aiserver.InitOptions) error { return initErr }, + inspect: func(_ context.Context, key string, c ldcontext.Context) aiserver.InspectResult { + calls = append(calls, inspectCall{key: key, ctx: c}) + return res + }, + } + return s, &calls +} + +func enabled(c aiserver.AiConfig) aiserver.InspectResult { + return aiserver.InspectResult{Enabled: true, Config: &c} +} + +func agentConfig(provider, model, instructions string) aiserver.AiConfig { + c := aiserver.AiConfig{Instructions: instructions, Model: aiserver.Model{Name: model}} + c.Provider.Name = provider + return c +} + +func TestAIConfigSource_AgentMode(t *testing.T) { + s, calls := fakeSource(nil, enabled(agentConfig("anthropic", "claude-sonnet-5", "Edit the entry point."))) + + cfg, err := s.Resolve(context.Background(), Task{SDKID: "node-server", Language: "JavaScript", Framework: "nextjs", EntryPoint: "app/layout.tsx", Dir: "/home/me/secret-project"}) + + require.NoError(t, err) + assert.Equal(t, "anthropic", cfg.ProviderID) + assert.Equal(t, "claude-sonnet-5", cfg.ModelID) + assert.Equal(t, "Edit the entry point.", cfg.Instructions, "the config's instructions are the system prompt") + assert.Contains(t, cfg.Task, "Follow your instructions", "the first message gives facts, not a task") + assert.Contains(t, cfg.Task, "- SDK: node-server") + assert.NotContains(t, cfg.Task, "Evaluate the boolean flag", "the CLI's own task is not sent") + require.Len(t, *calls, 1) + call := (*calls)[0] + assert.Equal(t, "setup", call.key) + assert.Equal(t, ldcontext.Kind("ldcli-setup"), call.ctx.Kind()) + assert.True(t, call.ctx.Anonymous()) + assert.Equal(t, "nextjs", call.ctx.GetValue("framework").StringValue()) + assert.Equal(t, "node-server", call.ctx.GetValue("sdkId").StringValue()) + assert.True(t, call.ctx.GetValue("hasEntryPoint").BoolValue()) + for _, name := range call.ctx.GetOptionalAttributeNames(nil) { + value := call.ctx.GetValue(name).StringValue() + assert.NotContains(t, value, "secret-project", "the context must not carry paths") + assert.NotContains(t, value, "layout.tsx", "the context must not carry paths") + } +} + +func TestAIConfigSource_CompletionModeUsesSystemMessages(t *testing.T) { + c := agentConfig("openai", "gpt-6", "") + c.Messages = []aiserver.Message{{Role: "system", Text: "One"}, {Role: "user", Text: "skip"}, {Role: "system", Text: "Two"}} + s, _ := fakeSource(nil, enabled(c)) + + cfg, err := s.Resolve(context.Background(), Task{}) + + require.NoError(t, err) + assert.Equal(t, "One\n\nTwo", cfg.Instructions) +} + +func TestAIConfigSource_MapsProviderNames(t *testing.T) { + s, _ := fakeSource(nil, enabled(agentConfig("Bedrock", "anthropic.claude", "x"))) + + cfg, err := s.Resolve(context.Background(), Task{}) + + require.NoError(t, err) + assert.Equal(t, "amazon-bedrock", cfg.ProviderID) +} + +func TestAIConfigSource_ModelWithoutProvider_UsesOpenCodeDefault(t *testing.T) { + s, _ := fakeSource(nil, enabled(agentConfig("", "claude-sonnet-5", ""))) + + cfg, err := s.Resolve(context.Background(), Task{}) + + require.NoError(t, err) + assert.Equal(t, Config{Instructions: DefaultInstructions}, cfg, "no instructions: the built-in task applies") +} + +func TestAIConfigSource_FallsBack(t *testing.T) { + for name, s := range map[string]*AIConfigSource{ + "init fails": first(fakeSource(errors.New("no network"), aiserver.InspectResult{})), + "config is off": first(fakeSource(nil, aiserver.InspectResult{Enabled: false})), + "config missing": first(fakeSource(nil, aiserver.InspectResult{Enabled: true})), + } { + t.Run(name, func(t *testing.T) { + cfg, err := s.Resolve(context.Background(), Task{}) + + require.NoError(t, err) + assert.Equal(t, Config{Instructions: DefaultInstructions}, cfg) + }) + } +} + +func TestAIConfigSource_InitsOnce(t *testing.T) { + inits := 0 + s := &AIConfigSource{ + RelayBaseURI: "https://app.launchdarkly.com", + checkRelay: noRelayCheck, + initClient: func(context.Context, aiserver.InitOptions) error { inits++; return nil }, + inspect: func(context.Context, string, ldcontext.Context) aiserver.InspectResult { + return aiserver.InspectResult{} + }, + } + + _, _ = s.Resolve(context.Background(), Task{}) + _, _ = s.Resolve(context.Background(), Task{}) + + assert.Equal(t, 1, inits) + assert.NoError(t, s.Close(context.Background()), "a source that did not start the SDK has nothing to stop") +} + +func first(s *AIConfigSource, _ *[]inspectCall) *AIConfigSource { return s } + +// noRelayCheck stands in for the request to the relay, so unit tests make no +// network calls. +func noRelayCheck(context.Context, string) (string, error) { return "", nil } diff --git a/internal/setup/agent/opencode.go b/internal/setup/agent/opencode.go index 1c89bf29..9c10292d 100644 --- a/internal/setup/agent/opencode.go +++ b/internal/setup/agent/opencode.go @@ -86,7 +86,11 @@ func (o *OpenCode) Start(ctx context.Context, task Task) (Run, error) { return nil, err } - run, err := startRun(ctx, srv.client, cfg, BuildPrompt(task)) + prompt := cfg.Task + if prompt == "" { + prompt = BuildPrompt(task) + } + run, err := startRun(ctx, srv.client, cfg, prompt) if err != nil { srv.stop() return nil, err diff --git a/internal/setup/agent/prompt.go b/internal/setup/agent/prompt.go index 6dab2069..ba39f947 100644 --- a/internal/setup/agent/prompt.go +++ b/internal/setup/agent/prompt.go @@ -46,6 +46,29 @@ Follow these rules: - Follow the code style of the project. - When you finish, list the files that you changed and say in one sentence how the user can see the flag value.` +// BuildFacts returns the first user message when the instructions come from +// the AI Config. It gives only the facts that the CLI detected and the choices +// that it made earlier in setup. It gives no task, so the config's +// instructions decide what the agent does. +func BuildFacts(t Task) string { + var b strings.Builder + b.WriteString("Follow your instructions to set up LaunchDarkly in the project in the current directory.\n\n") + b.WriteString("Facts from ldcli setup:\n") + writeFact(&b, "SDK", t.SDKID) + writeFact(&b, "Language", t.Language) + writeFact(&b, "Framework", t.Framework) + writeFact(&b, "Package manager", t.PackageManager) + writeFact(&b, "Entry point", t.EntryPoint) + installed := "no" + if t.SDKInstalled { + installed = "yes" + } + writeFact(&b, "SDK package installed", installed) + writeFact(&b, "Flag created by setup", t.FlagKey) + writeFact(&b, "Environment variable for the SDK credential", CredentialEnvVar(t.SDKID)) + return b.String() +} + // BuildPrompt returns the task message for one run. func BuildPrompt(t Task) string { var b strings.Builder diff --git a/internal/setup/agent/relay_proxy.go b/internal/setup/agent/relay_proxy.go new file mode 100644 index 00000000..433ece89 --- /dev/null +++ b/internal/setup/agent/relay_proxy.go @@ -0,0 +1,107 @@ +package agent + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "net/http/httputil" + "net/url" + "strings" + "time" +) + +const ( + // RelayPath is the path of the SDK relay in gonfalon, under the user's base + // URI. + RelayPath = "/internal/ldcli-setup-agent/sdk" + // RelayTokenHeader carries the user's access token to the relay. The relay + // cannot use Authorization, because the SDK puts its key there. The name + // must not start with X-LD-: gonfalon removes client request headers with + // that prefix, because its private auth headers use it. + RelayTokenHeader = "LD-CLI-Token" + // RelayPlaceholderKey is the SDK key that the CLI gives the AI SDK. The + // relay replaces it with the real key on the way to catfood. + RelayPlaceholderKey = "ldcli-placeholder" +) + +// relayProxy is a loopback HTTP server between the AI SDK and the relay. It +// adds the access token to each request, so the token is never in an SDK +// option, an environment variable, or the OpenCode process. The AI SDK cannot +// add a header to its trace exports, which is the other reason for the proxy. +// +// The proxy accepts requests only under a random path, so another process on +// the machine, such as OpenCode, cannot use it to call the relay with the +// user's token. +type relayProxy struct { + server *http.Server + listener net.Listener + // baseURL is the proxy address with the random path, for example + // http://127.0.0.1:52011/3f9a.... + baseURL string +} + +// startRelayProxy starts the proxy. relayBase is the user's base URI, for +// example https://app.launchdarkly.com. +func startRelayProxy(relayBase, accessToken string) (*relayProxy, error) { + target, err := url.Parse(strings.TrimRight(relayBase, "/") + RelayPath) + if err != nil || target.Scheme == "" || target.Host == "" { + return nil, fmt.Errorf("the base URI %q is not a valid URL", relayBase) + } + secret, err := randomHex(16) + if err != nil { + return nil, err + } + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, fmt.Errorf("could not start the relay proxy: %w", err) + } + + prefix := "/" + secret + proxy := &httputil.ReverseProxy{ + Rewrite: func(r *httputil.ProxyRequest) { + rest := strings.TrimPrefix(r.In.URL.Path, prefix) + r.Out.URL.Scheme = target.Scheme + r.Out.URL.Host = target.Host + r.Out.URL.Path = target.Path + rest + r.Out.URL.RawPath = "" + r.Out.Host = target.Host + r.Out.Header.Set(RelayTokenHeader, accessToken) + }, + // Send each part of a response as it arrives. The flag stream is a + // long-lived event stream, and a buffer would hold its events back. + FlushInterval: -1, + } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != prefix && !strings.HasPrefix(r.URL.Path, prefix+"/") { + http.NotFound(w, r) + return + } + proxy.ServeHTTP(w, r) + }) + + p := &relayProxy{ + server: &http.Server{ + Handler: handler, + ReadHeaderTimeout: 10 * time.Second, + }, + listener: listener, + baseURL: "http://" + listener.Addr().String() + prefix, + } + go func() { + if err := p.server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { + _ = listener.Close() + } + }() + return p, nil +} + +// close stops the proxy and ends open connections, including the flag +// stream. +func (p *relayProxy) close(ctx context.Context) error { + if err := p.server.Shutdown(ctx); err != nil { + return p.server.Close() + } + return nil +} diff --git a/internal/setup/agent/relay_proxy_test.go b/internal/setup/agent/relay_proxy_test.go new file mode 100644 index 00000000..11bbc0ae --- /dev/null +++ b/internal/setup/agent/relay_proxy_test.go @@ -0,0 +1,100 @@ +package agent + +import ( + "bufio" + "context" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fakeRelay records the last request and answers like the relay. +func fakeRelay(t *testing.T) (*httptest.Server, *http.Request) { + t.Helper() + got := &http.Request{} + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + *got = *r.Clone(context.Background()) + if strings.HasSuffix(r.URL.Path, "/stream/all") { + w.Header().Set("Content-Type", "text/event-stream") + _, _ = io.WriteString(w, "event: put\ndata: {}\n\n") + w.(http.Flusher).Flush() + <-r.Context().Done() + return + } + _, _ = io.WriteString(w, `{"flags":{}}`) + })) + t.Cleanup(srv.Close) + return srv, got +} + +func startTestProxy(t *testing.T, relayBase string) *relayProxy { + t.Helper() + p, err := startRelayProxy(relayBase, "api-secret-token") + require.NoError(t, err) + t.Cleanup(func() { _ = p.close(context.Background()) }) + return p +} + +func TestRelayProxy_AddsTheTokenAndMapsThePath(t *testing.T) { + relay, got := fakeRelay(t) + p := startTestProxy(t, relay.URL+"/") + + req, err := http.NewRequest(http.MethodGet, p.baseURL+"/polling/sdk/latest-all?basis=1", nil) + require.NoError(t, err) + req.Header.Set("Authorization", RelayPlaceholderKey) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, `{"flags":{}}`, string(body)) + assert.Equal(t, RelayPath+"/polling/sdk/latest-all", got.URL.Path) + assert.Equal(t, "basis=1", got.URL.RawQuery) + assert.Equal(t, "api-secret-token", got.Header.Get(RelayTokenHeader)) + assert.Equal(t, RelayPlaceholderKey, got.Header.Get("Authorization")) +} + +func TestRelayProxy_RefusesRequestsWithoutTheSecretPath(t *testing.T) { + relay, got := fakeRelay(t) + p := startTestProxy(t, relay.URL) + host := strings.TrimSuffix(p.baseURL, p.baseURL[strings.LastIndex(p.baseURL, "/"):]) + + resp, err := http.Get(host + "/polling/sdk/latest-all") + require.NoError(t, err) + resp.Body.Close() + + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + assert.Empty(t, got.Header.Get(RelayTokenHeader), "a request without the secret never reaches the relay") +} + +func TestRelayProxy_StreamsWithoutBuffering(t *testing.T) { + relay, _ := fakeRelay(t) + p := startTestProxy(t, relay.URL) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.baseURL+"/stream/all", nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + // The relay holds the stream open, so this line arrives only if the proxy + // sends it on at once. + line, err := bufio.NewReader(resp.Body).ReadString('\n') + require.NoError(t, err) + assert.Equal(t, "event: put\n", line) +} + +func TestStartRelayProxy_RejectsAnInvalidBaseURI(t *testing.T) { + _, err := startRelayProxy("not a url", "token") + + assert.Error(t, err) +} diff --git a/internal/setup/agent/relay_sdk_test.go b/internal/setup/agent/relay_sdk_test.go new file mode 100644 index 00000000..f8ebd8cd --- /dev/null +++ b/internal/setup/agent/relay_sdk_test.go @@ -0,0 +1,114 @@ +package agent + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// setupAgentFlag is the flag behind an agent-mode AI Config, as the stream +// sends it. +const setupAgentFlag = `{"key":"ldcli-setup-agent","on":true,"version":1,"salt":"s", +"fallthrough":{"variation":0},"offVariation":0, +"variations":[{"_ldMeta":{"enabled":true,"variationKey":"v1","mode":"agent","version":1}, +"model":{"name":"claude-sonnet-5"},"provider":{"name":"anthropic"}, +"instructions":"From AgentControl."}]}` + +// TestAIConfigSource_RealSDKThroughTheProxy starts the real AI SDK through the +// loopback proxy against a fake relay. It checks that the SDK uses the relay +// paths, sends the placeholder key and the token, and reads the config. +func TestAIConfigSource_RealSDKThroughTheProxy(t *testing.T) { + var mu sync.Mutex + var paths []string + relay := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + paths = append(paths, r.Method+" "+r.URL.Path) + mu.Unlock() + if r.Header.Get(RelayTokenHeader) != "api-123" || r.Header.Get("Authorization") != RelayPlaceholderKey { + w.WriteHeader(http.StatusUnauthorized) + return + } + if r.URL.Path == RelayPath+"/polling/sdk/latest-all" { + w.Header().Set("Content-Type", "application/json") + w.Header().Set(RelayEnvironmentIDHeader, "catfood-client-id") + fmt.Fprintf(w, `{"flags":{"ldcli-setup-agent":%s},"segments":{}}`, strings.ReplaceAll(setupAgentFlag, "\n", "")) + return + } + _, _ = io.Copy(io.Discard, r.Body) + w.WriteHeader(http.StatusAccepted) + })) + defer relay.Close() + t.Setenv(environmentIDEnv, "the-users-own-app") + + source := NewAIConfigSource(relay.URL, "api-123") + closer := source.(*AIConfigSource) + defer func() { _ = closer.Close(context.Background()) }() + + cfg, err := source.Resolve(context.Background(), Task{SDKID: "node-server"}) + + require.NoError(t, err) + assert.Equal(t, "anthropic/claude-sonnet-5", cfg.ProviderID+"/"+cfg.ModelID) + assert.Equal(t, "From AgentControl.", cfg.Instructions) + assert.Equal(t, "catfood-client-id", os.Getenv(environmentIDEnv), "the ID from the relay header reaches the SDK") + require.NoError(t, closer.Close(context.Background())) + mu.Lock() + defer mu.Unlock() + for _, p := range paths { + assert.Contains(t, p, RelayPath+"/", "every request goes to a relay route") + } + joined := strings.Join(paths, "\n") + assert.Contains(t, joined, "GET "+RelayPath+"/polling/sdk/latest-all", "the SDK polls") + assert.NotContains(t, joined, RelayPath+"/stream/", "the SDK does not open the flag stream") + t.Logf("requests: %v", paths) +} + +// TestAIConfigSource_RelayRefuses_FallsBack checks that a refusal, such as the +// kill switch being off, ends setup's wait at once and uses the built-in +// configuration. +func TestAIConfigSource_RelayRefuses_FallsBack(t *testing.T) { + relay := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = io.WriteString(w, `{"code":"ldcli_setup_agent_disabled"}`) + })) + defer relay.Close() + + source := NewAIConfigSource(relay.URL, "api-123").(*AIConfigSource) + defer func() { _ = source.Close(context.Background()) }() + + start := time.Now() + cfg, err := source.Resolve(context.Background(), Task{}) + + require.NoError(t, err) + assert.Equal(t, Config{Instructions: DefaultInstructions}, cfg) + assert.Less(t, time.Since(start), time.Second, "a refusal does not wait for the SDK start timeout") + var refusal *RelayError + require.ErrorAs(t, source.initErr, &refusal) + assert.Equal(t, "ldcli_setup_agent_disabled", refusal.Code) +} + +func TestAIConfigSource_RelayUnreachable_FallsBack(t *testing.T) { + relay := httptest.NewServer(http.NotFoundHandler()) + url := relay.URL + relay.Close() + + source := NewAIConfigSource(url, "api-123").(*AIConfigSource) + defer func() { _ = source.Close(context.Background()) }() + + start := time.Now() + cfg, err := source.Resolve(context.Background(), Task{}) + + require.NoError(t, err) + assert.Equal(t, Config{Instructions: DefaultInstructions}, cfg) + assert.Less(t, time.Since(start), time.Second) +}