diff --git a/cmd/generate_changelog/README.md b/cmd/generate_changelog/README.md index c7bc82ab..8ab57117 100644 --- a/cmd/generate_changelog/README.md +++ b/cmd/generate_changelog/README.md @@ -191,7 +191,7 @@ Create a `.env` file next to the `generate_changelog` binary: ```bash GITHUB_TOKEN=your_github_token_here -FABRIC_CHANGELOG_SUMMARIZE_MODEL=claude-sonnet-4-20250514 +FABRIC_CHANGELOG_SUMMARIZE_MODEL=gpt-5.6-sol ``` The tool automatically loads `.env` files for convenient configuration management. @@ -226,8 +226,8 @@ The tool can generate AI-powered summaries using Fabric for more polished, profe # Enable AI summarization generate_changelog --ai-summarize -# Custom model (default: claude-sonnet-4-20250514) -FABRIC_CHANGELOG_SUMMARIZE_MODEL=claude-opus-4 generate_changelog --ai-summarize +# Custom model (default: claude-opus-5) +FABRIC_CHANGELOG_SUMMARIZE_MODEL=gpt-5.6-sol generate_changelog --ai-summarize ``` ### AI Summary Features diff --git a/cmd/generate_changelog/incoming/2200.txt b/cmd/generate_changelog/incoming/2200.txt new file mode 100644 index 00000000..5a7273f3 --- /dev/null +++ b/cmd/generate_changelog/incoming/2200.txt @@ -0,0 +1,9 @@ +GitHub unexpected status code: 410 from provider GitHub, response body: {"error":{"code":"github_models_retirement_brownout","message":"GitHub Models is temporarily unavailable as part of a scheduled retirement brownout."}} + +### PR [#2200](https://github.com/danielmiessler/Fabric/pull/2200) by [ksylvan](https://github.com/ksylvan): fix: persist Codex OAuth tokens after refresh + +- Fixed Codex OAuth token persistence so refreshed credentials are securely retained across processes. +- Persisted rotated Codex credentials atomically using cross-process locks, and reloaded stored tokens before refresh to prevent stale rotations. +- Preserved existing environment values while enforcing secret-only file permissions. +- Reactivated configured vendors and surfaced configuration failures immediately. +- Reused valid refresh tokens before launching interactive authentication. diff --git a/cmd/generate_changelog/internal/changelog/summarize.go b/cmd/generate_changelog/internal/changelog/summarize.go index e1ada377..d4ad241a 100644 --- a/cmd/generate_changelog/internal/changelog/summarize.go +++ b/cmd/generate_changelog/internal/changelog/summarize.go @@ -7,7 +7,7 @@ import ( "strings" ) -const DefaultSummarizeModel = "claude-sonnet-4-5" +const DefaultSummarizeModel = "claude-opus-5" const MinContentLength = 256 // Minimum content length to consider for summarization const prompt = `# ROLE diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 5d7375f5..9f246778 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -55,6 +55,7 @@ Use parameterized queries instead of string concatenation... - Store secrets in environment variables or secure configuration - Use the built-in setup process for key management - Regularly rotate API keys +- `~/.config/fabric/.env` holds API keys and Codex OAuth tokens. Fabric writes it with mode `0600`. Treat the file as secret material. Comments and key order are not preserved when Codex tokens are updated in place. ### Input Validation diff --git a/go.mod b/go.mod index 13fe247b..2805d08c 100644 --- a/go.mod +++ b/go.mod @@ -4,61 +4,62 @@ go 1.26.0 require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 - github.com/anthropics/anthropic-sdk-go v1.61.0 + github.com/anthropics/anthropic-sdk-go v1.67.0 github.com/atotto/clipboard v0.1.4 - github.com/aws/aws-sdk-go-v2 v1.43.0 - github.com/aws/aws-sdk-go-v2/config v1.32.31 - github.com/aws/aws-sdk-go-v2/service/bedrock v1.66.0 - github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.56.0 + github.com/aws/aws-sdk-go-v2 v1.44.0 + github.com/aws/aws-sdk-go-v2/config v1.32.40 + github.com/aws/aws-sdk-go-v2/service/bedrock v1.68.0 + github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.58.0 github.com/gabriel-vasile/mimetype v1.4.15 github.com/gin-gonic/gin v1.12.0 - github.com/go-git/go-git/v5 v5.19.1 + github.com/go-git/go-git/v5 v5.19.2 github.com/go-shiori/go-readability v0.0.0-20251205110129-5db1dc9836f0 github.com/google/go-github/v66 v66.0.0 github.com/hasura/go-graphql-client v0.16.0 github.com/jessevdk/go-flags v1.6.1 github.com/joho/godotenv v1.5.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/mattn/go-sqlite3 v1.14.48 + github.com/mattn/go-sqlite3 v1.14.50 github.com/nicksnyder/go-i18n/v2 v2.6.1 - github.com/ollama/ollama v0.32.3 + github.com/ollama/ollama v0.33.1 github.com/openai/openai-go v1.12.0 github.com/otiai10/copy v1.14.1 github.com/pkg/errors v0.9.1 github.com/samber/lo v1.53.0 github.com/sgaunet/perplexity-go/v2 v2.16.1 github.com/spf13/cobra v1.10.2 - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.1 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/swag v1.16.6 golang.org/x/oauth2 v0.36.0 - golang.org/x/text v0.40.0 - google.golang.org/api v0.290.0 + golang.org/x/sys v0.47.0 + golang.org/x/text v0.41.0 + google.golang.org/api v0.294.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.9.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.6.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/buger/jsonparser v1.2.0 // indirect + github.com/buger/jsonparser v1.6.1 // indirect github.com/bytedance/gopkg v0.1.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-openapi/jsonpointer v1.0.0 // indirect - github.com/go-openapi/jsonreference v1.0.0 // indirect - github.com/go-openapi/spec v0.22.9 // indirect - github.com/go-openapi/swag/conv v0.27.3 // indirect - github.com/go-openapi/swag/jsonutils v0.27.3 // indirect - github.com/go-openapi/swag/loading v0.27.3 // indirect - github.com/go-openapi/swag/pools v0.27.3 // indirect - github.com/go-openapi/swag/stringutils v0.27.3 // indirect - github.com/go-openapi/swag/typeutils v0.27.3 // indirect - github.com/go-openapi/swag/yamlutils v0.27.3 // indirect + github.com/go-openapi/jsonreference v1.0.1 // indirect + github.com/go-openapi/spec v0.22.11 // indirect + github.com/go-openapi/swag/conv v0.29.1 // indirect + github.com/go-openapi/swag/jsonutils v0.29.1 // indirect + github.com/go-openapi/swag/loading v0.29.1 // indirect + github.com/go-openapi/swag/pools v0.29.1 // indirect + github.com/go-openapi/swag/stringutils v0.29.1 // indirect + github.com/go-openapi/swag/typeutils v0.29.1 // indirect + github.com/go-openapi/swag/yamlutils v0.29.1 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -72,18 +73,18 @@ require ( github.com/quic-go/quic-go v0.61.0 // indirect github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - go.mongodb.org/mongo-driver/v2 v2.8.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect + go.mongodb.org/mongo-driver/v2 v2.8.2 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect - golang.org/x/mod v0.38.0 // indirect + golang.org/x/mod v0.40.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.48.0 // indirect + golang.org/x/tools v0.49.0 // indirect ) require ( cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/auth v0.22.0 // indirect + cloud.google.com/go/auth v0.23.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect dario.cat/mergo v1.0.2 // indirect @@ -91,20 +92,20 @@ require ( github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/andybalholm/cascadia v1.3.4 // indirect github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.30 - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 // indirect - github.com/aws/smithy-go v1.27.4 // indirect - github.com/bytedance/sonic v1.15.2 // indirect - github.com/bytedance/sonic/loader v0.5.1 // indirect - github.com/cloudflare/circl v1.6.4 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.39 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40 // 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.13.40 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.34.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.46.0 // indirect + github.com/aws/smithy-go v1.28.1 // indirect + github.com/bytedance/sonic v1.15.3 // indirect + github.com/bytedance/sonic/loader v0.5.2 // indirect + github.com/cloudflare/circl v1.6.5 // indirect github.com/cloudwego/base64x v0.1.7 // indirect github.com/coder/websocket v1.8.15 // indirect github.com/cyphar/filepath-securejoin v0.7.0 // indirect @@ -126,8 +127,8 @@ require ( github.com/google/go-querystring v1.2.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.19 // indirect - github.com/googleapis/gax-go/v2 v2.23.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.21 // indirect + github.com/googleapis/gax-go/v2 v2.24.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -149,21 +150,20 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.3.1 // indirect + github.com/ugorji/go/codec v1.3.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect - go.opentelemetry.io/otel v1.44.0 // indirect - go.opentelemetry.io/otel/metric v1.44.0 // indirect - go.opentelemetry.io/otel/trace v1.44.0 // indirect - golang.org/x/arch v0.29.0 // indirect - golang.org/x/crypto v0.54.0 // indirect - golang.org/x/net v0.57.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect + go.opentelemetry.io/otel v1.46.0 // indirect + go.opentelemetry.io/otel/metric v1.46.0 // indirect + go.opentelemetry.io/otel/trace v1.46.0 // indirect + golang.org/x/arch v0.30.0 // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - google.golang.org/genai v1.65.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df // indirect - google.golang.org/grpc v1.82.1 // indirect - google.golang.org/protobuf v1.36.11 // indirect + google.golang.org/genai v1.70.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 0cb2022b..fe298eed 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/auth v0.22.0 h1:Xp9wAKkLoeaYb5pYZZoQGz4E9sdPxIbzS3gywZE3ciQ= cloud.google.com/go/auth v0.22.0/go.mod h1:M9o2Oz+YI2jAfxewJgb1vyI3vceHF+eohmxyzmrl+9s= +cloud.google.com/go/auth v0.23.2 h1:pxSCpfiji41hpzpPdMCftEUCezpgpqmmDdYiAjCKXxo= +cloud.google.com/go/auth v0.23.2/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= @@ -10,6 +12,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 h1:4gRPBpN1f6xt88yi4WR26m7XaD9OlWtVT6bWPdGUIok= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0/go.mod h1:G7QVLxw1j1JVyrO1MA95S8m8HStaaleDZYTcfGgjB2o= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0/go.mod h1:q0+UTSRvShwUCrR/s5HtyInYphN7Wvxb7snFM3u+SLA= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs= @@ -20,6 +24,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU= github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.9.0 h1:MDT4FxAPve5FnYn6vOL1r7RCRDG+l9cI7a5LlCuHsqA= +github.com/AzureAD/microsoft-authentication-library-for-go v1.9.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= @@ -35,6 +41,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/anthropics/anthropic-sdk-go v1.61.0 h1:JRTnm1tPqn5xo1xd1zfrcFDlcoWXVMvV1K68YmhpZKw= github.com/anthropics/anthropic-sdk-go v1.61.0/go.mod h1:3EfIfmFqxH6rbiLcIP4tPFyXL/IHakx2wDG4OU+TIEI= +github.com/anthropics/anthropic-sdk-go v1.67.0 h1:tTsVSR+YD6R+1EPoMyZ9im7YKEjJpYGZk5klcDi9HRg= +github.com/anthropics/anthropic-sdk-go v1.67.0/go.mod h1:3EfIfmFqxH6rbiLcIP4tPFyXL/IHakx2wDG4OU+TIEI= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -43,52 +51,94 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go-v2 v1.43.0 h1:fharf/WhbRAVZ1du0QL7roNFxZ6T/sWr+4Ni617bwSI= github.com/aws/aws-sdk-go-v2 v1.43.0/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM= +github.com/aws/aws-sdk-go-v2 v1.44.0 h1:4IbaHhtzy+4h37z4JQyO9a2QsiCml3CNYHtq5hIHigo= +github.com/aws/aws-sdk-go-v2 v1.44.0/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 h1:3IZY0XAJquT3aHzbkHfPzy4ACPcEjVG0x87KOwtpqGY= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14/go.mod h1:zwM6veDkhGgQFqkBy+uT28AAYpLu+uFMlPl+rCg/73E= +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.32.31 h1:n4nY9O3QKoHIkL85EX+V8RcMFtOhlpTFhGArg915PXk= github.com/aws/aws-sdk-go-v2/config v1.32.31/go.mod h1:PN0NYDCCoOpGGsZ2+elDUidmHfQBPyYzN2GCgl8HEBs= +github.com/aws/aws-sdk-go-v2/config v1.32.40 h1:lAVC9gMmKusmqDRe32dPtgKl/BWvJmMJoWELKHCAObw= +github.com/aws/aws-sdk-go-v2/config v1.32.40/go.mod h1:8xOJLbe/hOj1g4PVsfJYV7O2byq+UGET1onDdUgbwqc= github.com/aws/aws-sdk-go-v2/credentials v1.19.30 h1:TTCvvzFU6gXa4iJecNG/0F/B0oYTiazoRECr2XyLHrY= github.com/aws/aws-sdk-go-v2/credentials v1.19.30/go.mod h1:jKxAp2AEncnliinzpgOSZDFv6+VjvWhjw/AtbfsWT9U= +github.com/aws/aws-sdk-go-v2/credentials v1.19.39 h1:XOg8LC3Kgnsa3WiPQjc7Bi8k5IBN92cPYfIV9XMFss0= +github.com/aws/aws-sdk-go-v2/credentials v1.19.39/go.mod h1:GonTDBQ+mTpCVNwaHjj0PagspfrYYMEqOx7FehoEP/I= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 h1:kfVL5wAunCJycL6MOQ6aNh6PlAYEymflcjuKmrWUA0o= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31/go.mod h1:nWfRNDAppujCQgOUd43lKT4yeLv9z3nJ3bw1G3BgQKo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40 h1:r5aGipEVgI9aT/tAGjdrPbDQvIAKdTrS3rUPQtG4Rmo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40/go.mod h1:vOD3CnPxAdkL6MWZeROkZsTlskklMFfgVFkHzx/oZpY= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31 h1:Z8F3hfCY33IGpJjFAnv0wvtv1FIKj1GHmRDEYqy64tw= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31/go.mod h1:aVyUoytEyOViR6jhq6jula0xkc5NfBE2hgeF6BvOrao= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40 h1:UIXlbijuB2XK1Kr57fo8iIxCuaSHJzwZ1uo+2tbEYIk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40/go.mod h1:wcEsL6jscjZjVUinb0Q5qD/GXOG1yT3GNfmT9HuDwzU= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31 h1:hyOxUyXdh3AyjE93gBgsfziJag9ACwcs+ZpDBLzi8mw= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31/go.mod h1:OERqI9k0draSLB8O8woxY3q25ZWTELRK4RRoLMuMZFo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40 h1:xLQVRDs2NddDmK9BEyh5KSlJ1Gpy5/GIJXrV6WcVGAE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40/go.mod h1:XRXnpFVFGLaEVK+olDdFIM1vNa04ETW452oFGEPUxAo= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32 h1:0MrUL35H/Y4kdFfItoR5jCgtDQ4Z/8LudAoIHRfA4hE= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32/go.mod h1:2tNZkuWz54arj8mHVf+8Y7cKkcD8Wr/fBpENgEXpjLc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41 h1:nv/ILuCY0yXACzMQwvtt/HbqDDjemZiI0AeDbxGQlnU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41/go.mod h1:dzvOSpxaPqQ3j0xS6Lc1vyVuWW0RBj7s/QqYpzu3Q/0= github.com/aws/aws-sdk-go-v2/service/bedrock v1.66.0 h1:RMRC4k42Bt3wHqWiPyFfiRrOyRhJZW7w6vAw5ZnMrPM= github.com/aws/aws-sdk-go-v2/service/bedrock v1.66.0/go.mod h1:1asCJhndokTo9QEJ68/eZv4yJsRPV9A0ROdDx7QQBkQ= +github.com/aws/aws-sdk-go-v2/service/bedrock v1.68.0 h1:2T9SsmNe78eLtmobdnvxk088DVuUfYp9Ml9MxFMWQUQ= +github.com/aws/aws-sdk-go-v2/service/bedrock v1.68.0/go.mod h1:ANOxqA/+nqohBqirnFCF8EqJFkjAbTLYfyBl6NEjNug= github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.56.0 h1:CWw8zDpnMJLwSvZd41Ncf/eJPeZ5t74UxGAu4HbM3S4= github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.56.0/go.mod h1:dGxTgK2ZKWrbZv5o/8oCeO3Uch3n0w2rtSFroeJoLcE= +github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.58.0 h1:TDwZrhBZTHNxvGiqqDoNjdUuoveRRVfy14VeFHbbWBc= +github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.58.0/go.mod h1:ZnrFfnjYjXc/PC2a2hwAIS2qf1Yqk15EMLryhca2wps= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ= +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.13.31 h1:w2SIhW92DZPFrSL4ksVCr8IYff5OZwIcxg8+95tzvAI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31/go.mod h1:wAhpCQbkov+IcvjozJbd2xRCoZybUEHNkcFunssNACg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40 h1:gr3Fw1cxZXNCdeo/lQ7isHEHzvHVM7z75qb2zW9aMjw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40/go.mod h1:8z/9CmfnQhiuXD7Ykbcg4a/whSWsniE0ODSx9uwVzfk= github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 h1:OHH5iTQvVGmfHjX/5Q+vFuA/Rf2x6/95aJ/75QCQSm4= github.com/aws/aws-sdk-go-v2/service/signin v1.5.0/go.mod h1:mCF3AK9PpL49oOrhniUXWAfhVBVQ/XbytoE5eccZUIs= +github.com/aws/aws-sdk-go-v2/service/signin v1.6.0 h1:agcr0j8YeFEzdXNo17Rg9MbbjLRjrimabwNtji4e+lU= +github.com/aws/aws-sdk-go-v2/service/signin v1.6.0/go.mod h1:qU5PxgQ4JiUOOMotzfO3+5oUda5W+8JDVKyLQqlrJik= github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 h1:CaJyYhxBE0M/HJX/YvSaSmQlsI91VHB0lKU8LtLxL3A= github.com/aws/aws-sdk-go-v2/service/sso v1.33.0/go.mod h1:+e6BMRMPjBQoCw/WovYR9GLy2IU0z4Q77smOB1DraSg= +github.com/aws/aws-sdk-go-v2/service/sso v1.34.0 h1:FxaN8/sn61DTXNI6Gt678tFJUY8iUsCchm6Y/F/RjaA= +github.com/aws/aws-sdk-go-v2/service/sso v1.34.0/go.mod h1:vu4OY6s8LJtT8BtYG2LD6BGSZMptkYn3o5hvCPB22jc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 h1:tC323YV77QdafeBr6LUhLDTsboyuyHLNRwAyCP44kGU= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0/go.mod h1:SfLK1sgviHmbI+MozR9iDwDjL4cdCVZtahsjoR+z7wg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0 h1:crWKPeGYTBTuBxQ3p73kjfJvt4brUIsr+Fuypko8FxY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0/go.mod h1:HjjZVhaBz0JBR/kbWKThmNDhFKS7y6EURuk493tJk9Y= github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 h1:Pd6PNlp4t8PTXxqzstICl52Wsy78vpjFZ7PRUj44mJc= github.com/aws/aws-sdk-go-v2/service/sts v1.45.0/go.mod h1:rmQ0TnHzuLPmabgjPcsywhsSOmaBDgzR4zvDxSPsGdg= +github.com/aws/aws-sdk-go-v2/service/sts v1.46.0 h1:IZ63JdogSNNjex/jsODNv7jGDcO/xJYd9FsgyfCsp1g= +github.com/aws/aws-sdk-go-v2/service/sts v1.46.0/go.mod h1:I+rwAf3spG5dITBaAo3xXRowk8kiOhtU1kYxfvCTC44= github.com/aws/smithy-go v1.27.4 h1:JQcphmBN4f0q/sPqXqROIItRNV/hy10cgu7CsFy616M= github.com/aws/smithy-go v1.27.4/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +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/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g= github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/buger/jsonparser v1.6.1 h1:I0phFv0PlbLHnM7TZAVjZ2MJ2/eWRTDyuO7GLR98IEs= +github.com/buger/jsonparser v1.6.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo= github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA= +github.com/bytedance/sonic v1.15.3 h1:P3akjLPBtV/i6bHC6LbcLjY3KuoOvfiqF8wFHeP5IhY= +github.com/bytedance/sonic v1.15.3/go.mod h1:8e51yTPdY8M6t+vvGL1c2Y1xL9i+frEeIAQAEl75NUc= github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI= github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/bytedance/sonic/loader v0.5.2 h1:0QtP1gevc1OZ6/H8Lb9BRZiCXd1Ftjd3OKuj1T1lBIo= +github.com/bytedance/sonic/loader v0.5.2/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= 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/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U= github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY= +github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA= +github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c= github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI= github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= @@ -133,6 +183,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY= +github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk= 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= @@ -142,29 +194,50 @@ github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxg github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY= github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0= +github.com/go-openapi/jsonreference v1.0.1 h1:4zJ7AmYDKNmD3aSpfPnFNCFA5E80/xMHUNKgydaLh38= +github.com/go-openapi/jsonreference v1.0.1/go.mod h1:dYplQXa6p5lXprLcJ8LE2iU7vNpXsAHDQ5ZAgL+Qx3A= github.com/go-openapi/spec v0.22.9 h1:/vKIFDcGKp0ktZWGbym/tJEWbk6/XOEmAVU0kqKMH+w= github.com/go-openapi/spec v0.22.9/go.mod h1:b/mNUYIOQOyIiUzUzXEE8xzyZqf93KvM9hQGP91yfl0= +github.com/go-openapi/spec v0.22.11 h1:/ZW28n4PCghbBpwjmvoMeyCeDwd6pIe715Ov8T3kAaQ= +github.com/go-openapi/spec v0.22.11/go.mod h1:Ypu+u1KTejey7/QCr3j1ChdjwhGgXuklbH7l8+YP1J4= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag/conv v0.27.3 h1:iqJFmGEjmX3AY0lSszABFqRVqOSt99XS0LzNIMJYuhU= github.com/go-openapi/swag/conv v0.27.3/go.mod h1:nPRmN6jgNme99hpf+nM0auDZGALWIqlwhisKPK/bQhQ= +github.com/go-openapi/swag/conv v0.29.1 h1:AC4Eh/5c/eUDOUCzzsRC9ghmFgOSBHeRMGIngY0ZUGA= +github.com/go-openapi/swag/conv v0.29.1/go.mod h1:S1X7/ZrBEZOC0Wc8AGxjbcGS92l3WEjA7aPtpl+RaqM= github.com/go-openapi/swag/jsonutils v0.27.3 h1:1DEz+O82frtSMBcos/7XIn1GnpNTbsD4Bru4Dc/uhRc= github.com/go-openapi/swag/jsonutils v0.27.3/go.mod h1:qiDCoQvzkMxrV3G8FLEdIU5L+EFYc0zcDOHWT3Yofvo= +github.com/go-openapi/swag/jsonutils v0.29.1 h1:AFCxs0eQZ24/QyfhVHM2t49rMz7Vv3XCsZQI6yrNy+c= +github.com/go-openapi/swag/jsonutils v0.29.1/go.mod h1:u3+sCfJpttDpcmS5kpm0yxL6GK0eWgODsx8Yw8fcqNM= github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3 h1:h/eT9kmGCDdFLJF29lOhzLtF0FmP1AX2MhLJWVebsb8= github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.1 h1:BiiXE31Bx9SfpsMmOQj5KYpUhTZBpLVriVhJDuLuY2o= github.com/go-openapi/swag/loading v0.27.3 h1:L9nQkEgzU7QgFQL+pLEMfGUKxeM4pWwGwbET9Z3weW0= github.com/go-openapi/swag/loading v0.27.3/go.mod h1:rJ0NeaKsF4CVPnMGjPQl7JlSHzvD0bc2DKXLss1hiuE= +github.com/go-openapi/swag/loading v0.29.1 h1:FCv5fG8UhTdDJa2R7w+5O9Ekpcbw7tt0nFWvmDKGBjc= +github.com/go-openapi/swag/loading v0.29.1/go.mod h1:N0ESuem4p2oedKal8EJhciqnJ9Q9Wmt83L1CRB3Fouw= github.com/go-openapi/swag/pools v0.27.3 h1:gXjImP3F6/56wRRcFgEPld084Y6u2gs21ikPBt8NKBk= github.com/go-openapi/swag/pools v0.27.3/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE= +github.com/go-openapi/swag/pools v0.29.1 h1:NRogYxdEW9SjRM4mkAOji9iefO4MRXq3p/ZJcoQbUKg= +github.com/go-openapi/swag/pools v0.29.1/go.mod h1:leDcaghjkRAhCuCRv9NfJU5f0mjoU3cT/XZObhMk3pc= github.com/go-openapi/swag/stringutils v0.27.3 h1:Ru28hnbAvN5wycALQYy8IobHvASq+FUFMlp1QzLM0JI= github.com/go-openapi/swag/stringutils v0.27.3/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= +github.com/go-openapi/swag/stringutils v0.29.1 h1:1ykunK7iJQk1uOO7+oUH1ukbsK85fFCOiCFMOVSY+F0= +github.com/go-openapi/swag/stringutils v0.29.1/go.mod h1:7fSqZ+z8Qc0tOfAAK0jVa5qFGrnIlRi6n7NeGGrr1vc= github.com/go-openapi/swag/typeutils v0.27.3 h1:l6SSrx5eR5/WVwrGNzN6bQ9WqL04mrxNBl9YgQ3rcJ4= github.com/go-openapi/swag/typeutils v0.27.3/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= +github.com/go-openapi/swag/typeutils v0.29.1 h1:Nzv9nhnlLCRBPQqfOX+7lB6Guju370or8StT+lIOf6M= +github.com/go-openapi/swag/typeutils v0.29.1/go.mod h1:hxpgDZJVBkBsi/d3MIUosafoFdE5exaQRmVp0zwu3YE= github.com/go-openapi/swag/yamlutils v0.27.3 h1:cRFCAoYtslYn9L9T0xWryHy1t7c1MACC+DMj3CLvwvs= github.com/go-openapi/swag/yamlutils v0.27.3/go.mod h1:6JYBGj8sw/NawMllyZY+cTA8Mzk2etS3ZBASdcyPsiU= +github.com/go-openapi/swag/yamlutils v0.29.1 h1:69w3tsBajm7MR/fejLy7HD/3J68Ys1SeeZMEzZ3w2sk= +github.com/go-openapi/swag/yamlutils v0.29.1/go.mod h1:rgsp3vT/QdWzKwn43CigDwjOGIenPyTZMKnxEM8jZOA= github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.1 h1:Jm+/ze2rMtbD98yen92AhATGLGREDYXG56Xr4gMjEtE= 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-openapi/testify/v2 v2.6.1 h1:6CNJhTjMzgaeaH8WhshcsZNPIvRemiOcFpU7seO/y7Q= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -203,8 +276,12 @@ 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= github.com/googleapis/enterprise-certificate-proxy v0.3.19 h1:mMOE7DN2+p76/EdIrmAy9B9bH+yC4563vmnJ34QR8i4= github.com/googleapis/enterprise-certificate-proxy v0.3.19/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k= +github.com/googleapis/enterprise-certificate-proxy v0.3.21 h1:OFdQ3tnCX/zaQ0Cedur3D3z7kI6HiLX9g3TiAN4/DFU= +github.com/googleapis/enterprise-certificate-proxy v0.3.21/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w= github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE= github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg= +github.com/googleapis/gax-go/v2 v2.24.0 h1:myMaPYyF9MecEmvQqMqomIwn9t/4KCZN9qnwsS76wlg= +github.com/googleapis/gax-go/v2 v2.24.0/go.mod h1:IaTHBDd7NHxSCiu0vEs8pQZu4dGZrWwuSoxCnk16OFM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hasura/go-graphql-client v0.16.0 h1:DQLfp+djj4j5NPdJkGYym8J55hpm5etML1zqgco78Qc= @@ -247,6 +324,8 @@ github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= +github.com/mattn/go-sqlite3 v1.14.50 h1:dmdFvo1XG4MPzA4IkAmE9upVz/Nj31uRoM5+jC8hYbY= +github.com/mattn/go-sqlite3 v1.14.50/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -256,6 +335,8 @@ github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/ollama/ollama v0.32.3 h1:hASAqO6McQAgOhLG3Fcgrj//aXK5rMMWxqFM4eAKkGc= github.com/ollama/ollama v0.32.3/go.mod h1:b1ydCt2oVg0VAg22WWDgCbwW0AyOaRKAFzlS91NI4OY= +github.com/ollama/ollama v0.33.1 h1:nEeqiZzqlO+RfjJ7AvfUgrYlV/wbrxPl2szRg9mtxPU= +github.com/ollama/ollama v0.33.1/go.mod h1:Kekx/+OtFZHmqbkVH/QUUDVcMQS+1pg1dcz4Qy7TGn4= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0= @@ -320,6 +401,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= @@ -341,6 +424,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/ugorji/go/codec v1.3.2 h1:zkEASHHyEClGeURfgNT9PJZVfAbs9oEX9QXggwWNJbc= +github.com/ugorji/go/codec v1.3.2/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -348,40 +433,62 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8= go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.mongodb.org/mongo-driver/v2 v2.8.2 h1:b6o2m7zL8g2URuO8urBedAylxojybKXNZTxgkOcl+2w= +go.mongodb.org/mongo-driver/v2 v2.8.2/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= 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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 h1:B2h3uqicet1CT2N5TOFhS+Gq++9i0/CLmaxvhmhtP5s= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0/go.mod h1:dylvB+ZiiwMvsDij9O84Uy7SijLgHMX4mbkncds+4Sw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= +go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI= go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= +go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= 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= go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/arch v0.29.0 h1:8sSET5wB0+exBm0FGmOtdHMqjlRdV2DRD3/IV6OZgho= golang.org/x/arch v0.29.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/arch v0.30.0 h1:sB9h+1gRGa2+LauFSV0tm8bK1J2yo1bx6/Uyi/P6DTU= +golang.org/x/arch v0.30.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -389,6 +496,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -420,6 +529,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -427,23 +538,37 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +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= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.290.0 h1:eMw0Xo+IfbbMlKmW7aHvpyQRv9RCXuWx/vs8AD+0x9A= google.golang.org/api v0.290.0/go.mod h1:weJZ3lldHFYI0DBFNKpJelUDNnusTt5YaOEgxvt8ci8= +google.golang.org/api v0.294.0 h1:8gASjJxdtcIieB3OqbkLcF0FfbXVNqKtU5iozD1ssvA= +google.golang.org/api v0.294.0/go.mod h1:02qB8+Ox1ZFzcaKFMguy1nQLJmSIyvV6Ff4txJEXtl4= google.golang.org/genai v1.65.0 h1:6QK3Rjsx0iuJjbDCE1vf1VUr1IEjRDpvexGGnhxoAIk= google.golang.org/genai v1.65.0/go.mod h1:mDdPDFXo1Ats7f1WXVyZgWb/CkMzFWTWJruIMy7hGIU= +google.golang.org/genai v1.70.0 h1:V9oYOBvTDYbmeklOHjvxGeeLau71WiS2CWKrktQtDok= +google.golang.org/genai v1.70.0/go.mod h1:mDdPDFXo1Ats7f1WXVyZgWb/CkMzFWTWJruIMy7hGIU= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms= google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU= google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA= +google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d h1:QwnJwPte4XXAkhPu26LTDIahnsMSUV0kK8HkxbC+Pc4= google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df h1:O3ig1i5WDDzsVzRp+cCdgelT9vXnlnOFdlEeFtL4HCc= google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +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 v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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/core/plugin_registry.go b/internal/core/plugin_registry.go index c55feab8..a913f471 100644 --- a/internal/core/plugin_registry.go +++ b/internal/core/plugin_registry.go @@ -10,6 +10,7 @@ import ( "sort" "strconv" "strings" + "sync" "github.com/danielmiessler/fabric/internal/i18n" debuglog "github.com/danielmiessler/fabric/internal/log" @@ -73,6 +74,24 @@ func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry, err error) { vendors := []ai.Vendor{} // Add non-OpenAI compatible clients + codexClient := codex.NewClient() + codexClient.WithStoreLock = func(fn func() error) error { + return db.WithEnvLock(func() error { + env, err := db.ReadEnvFile() + if err != nil { + return err + } + codexClient.LoadEnvSettings(env) + return fn() + }) + } + codexClient.TokenPersist = func() error { + return db.ApplyEnvUpdates(map[string]string{ + codexClient.AccessToken.EnvVariable: strings.TrimSpace(codexClient.AccessToken.Value), + codexClient.RefreshToken.EnvVariable: strings.TrimSpace(codexClient.RefreshToken.Value), + codexClient.AccountID.EnvVariable: strings.TrimSpace(codexClient.AccountID.Value), + }) + } vendors = append(vendors, openai.NewClient(), digitalocean.NewClient(), @@ -86,7 +105,7 @@ func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry, err error) { lmstudio.NewClient(), exolab.NewClient(), perplexity.NewClient(), - codex.NewClient(), + codexClient, copilot.NewClient(), // Microsoft 365 Copilot bedrock.NewClient(), // AWS Bedrock - credentials configured via setup or AWS credential chain ) @@ -123,6 +142,8 @@ func (o *PluginRegistry) ListVendors(out io.Writer) error { type PluginRegistry struct { Db *fsdb.Db + vendorMu sync.Mutex + VendorManager *ai.VendorsManager VendorsAll *ai.VendorsManager Defaults *tools.Defaults @@ -288,16 +309,12 @@ func (o *PluginRegistry) runVendorSetup() (err error) { return pluginSetupErr } + o.registerVendor(plugin) + if err = o.SaveEnvFile(); err != nil { return } - if o.VendorManager.FindByName(plugin.GetName()) == nil { - if vendor, ok := plugin.(ai.Vendor); ok { - o.VendorManager.AddVendors(vendor) - } - } - return } @@ -349,16 +366,11 @@ func (o *PluginRegistry) runInteractiveSetup() (err error) { if pluginSetupErr := plugin.Setup(); pluginSetupErr != nil { println(pluginSetupErr.Error()) } else { + o.registerVendor(plugin) if err = o.SaveEnvFile(); err != nil { break } } - - if o.VendorManager.FindByName(plugin.GetName()) == nil { - if vendor, ok := plugin.(ai.Vendor); ok { - o.VendorManager.AddVendors(vendor) - } - } } else { break } @@ -425,11 +437,32 @@ func (o *PluginRegistry) SetupVendor(vendorName string) (err error) { if err = o.VendorsAll.SetupVendor(vendorName, o.VendorManager.VendorsByName); err != nil { return } + if vendor := o.VendorsAll.FindByName(vendorName); vendor != nil { + o.registerVendor(vendor) + } err = o.SaveEnvFile() return } +func (o *PluginRegistry) registerVendor(plugin plugins.Plugin) { + vendor, ok := plugin.(ai.Vendor) + if !ok { + return + } + o.vendorMu.Lock() + defer o.vendorMu.Unlock() + name := vendor.GetName() + for _, existing := range o.VendorManager.Vendors { + if strings.EqualFold(existing.GetName(), name) { + return + } + } + o.VendorManager.AddVendors(vendor) +} + func (o *PluginRegistry) ConfigureVendors() { + o.vendorMu.Lock() + defer o.vendorMu.Unlock() o.VendorManager.Clear() for _, vendor := range o.VendorsAll.Vendors { if vendorErr := vendor.Configure(); vendorErr == nil && vendor.IsConfigured() { @@ -438,6 +471,34 @@ func (o *PluginRegistry) ConfigureVendors() { } } +func (o *PluginRegistry) activateVendor(name string) (ai.Vendor, error) { + if strings.TrimSpace(name) == "" { + return nil, nil + } + + o.vendorMu.Lock() + defer o.vendorMu.Unlock() + + if v := o.VendorManager.FindByName(name); v != nil { + return v, nil + } + if o.VendorsAll == nil { + return nil, nil + } + v := o.VendorsAll.FindByName(name) + if v == nil { + return nil, nil + } + if err := v.Configure(); err != nil { + return nil, err + } + if !v.IsConfigured() { + return nil, nil + } + o.VendorManager.AddVendors(v) + return v, nil +} + func (o *PluginRegistry) GetModels() (ret *ai.VendorsModels, err error) { o.ConfigureVendors() ret, err = o.VendorManager.GetModels() @@ -503,13 +564,25 @@ func (o *PluginRegistry) GetChatter(model string, modelContextLength int, vendor ret.model = defaultModel } } else if model == "" { - if vendorName != "" { - ret.vendor = vendorManager.FindByName(vendorName) - } else { - ret.vendor = vendorManager.FindByName(defaultVendor) + name := vendorName + if name == "" { + name = defaultVendor + } + if ret.vendor, err = o.activateVendor(name); err != nil { + return } ret.model = defaultModel } else { + if vendorName != "" { + if ret.vendor, err = o.activateVendor(vendorName); err != nil { + return + } + } else if !vendorManager.HasVendors() { + if _, err = o.activateVendor(defaultVendor); err != nil { + return + } + } + var models *ai.VendorsModels if models, err = vendorManager.GetModels(); err != nil { return @@ -567,6 +640,16 @@ func (o *PluginRegistry) GetChatter(model string, modelContextLength int, vendor ret.model = model } + if ret.vendor == nil { + name := vendorName + if name == "" { + name = defaultVendor + } + if ret.vendor, err = o.activateVendor(name); err != nil { + return + } + } + if ret.vendor == nil { var errMsg string if defaultModel == "" || defaultVendor == "" { diff --git a/internal/core/plugin_registry_test.go b/internal/core/plugin_registry_test.go index 4989908a..97409df5 100644 --- a/internal/core/plugin_registry_test.go +++ b/internal/core/plugin_registry_test.go @@ -3,6 +3,7 @@ package core import ( "bytes" "context" + "errors" "io" "os" "strings" @@ -10,11 +11,14 @@ import ( "github.com/danielmiessler/fabric/internal/chat" "github.com/danielmiessler/fabric/internal/domain" + "github.com/danielmiessler/fabric/internal/i18n" debuglog "github.com/danielmiessler/fabric/internal/log" "github.com/danielmiessler/fabric/internal/plugins" "github.com/danielmiessler/fabric/internal/plugins/ai" + "github.com/danielmiessler/fabric/internal/plugins/ai/codex" "github.com/danielmiessler/fabric/internal/plugins/db/fsdb" "github.com/danielmiessler/fabric/internal/tools" + "github.com/joho/godotenv" ) func TestSaveEnvFile(t *testing.T) { @@ -32,14 +36,15 @@ func TestSaveEnvFile(t *testing.T) { // testVendor implements ai.Vendor for testing purposes type testVendor struct { - name string - models []string + name string + models []string + configureErr error } func (m *testVendor) GetName() string { return m.name } func (m *testVendor) GetSetupDescription() string { return m.name } func (m *testVendor) IsConfigured() bool { return true } -func (m *testVendor) Configure() error { return nil } +func (m *testVendor) Configure() error { return m.configureErr } func (m *testVendor) Setup() error { return nil } func (m *testVendor) SetupFillEnvFileContent(*bytes.Buffer) {} func (m *testVendor) ListModels(context.Context) ([]string, error) { return m.models, nil } @@ -210,3 +215,116 @@ func TestGetChatter_VendorPrefixIgnoredWhenNotAVendor(t *testing.T) { t.Fatalf("expected model 'notavendor/model', got %s", chatter.model) } } + +func TestGetChatter_ReportsConfigureError(t *testing.T) { + if _, err := i18n.Init("en"); err != nil { + t.Fatalf("i18n.Init() error = %v", err) + } + for _, tt := range []struct { + name, model, vendor string + }{ + {"empty model", "", ""}, + {"model", "gpt-5.6-sol", ""}, + {"named vendor", "gpt-5.6-sol", "Codex"}, + } { + t.Run(tt.name, func(t *testing.T) { + configureErr := errors.New(i18n.T("codex_login_refresh_failed")) + registry := newInactiveVendorRegistry(t, "Codex", "gpt-5.6-sol", &testVendor{ + name: "Codex", models: []string{"gpt-5.6-sol"}, configureErr: configureErr, + }) + _, err := registry.GetChatter(tt.model, 0, tt.vendor, false, false) + if !errors.Is(err, configureErr) { + t.Fatalf("GetChatter() error = %v, want %v", err, configureErr) + } + }) + } +} + +func TestGetChatter_ActivatesInactiveVendor(t *testing.T) { + for _, tt := range []struct { + name, model, vendor string + }{ + {"named", "gpt-5.6-sol", "Codex"}, + {"default for model", "gpt-5.6-sol", ""}, + } { + t.Run(tt.name, func(t *testing.T) { + codexVendor := &testVendor{name: "Codex", models: []string{"gpt-5.6-sol"}} + registry := newInactiveVendorRegistry(t, "Codex", "gpt-5.6-sol", codexVendor) + chatter, err := registry.GetChatter(tt.model, 0, tt.vendor, false, false) + if err != nil { + t.Fatalf("GetChatter() error = %v", err) + } + if chatter.vendor.GetName() != "Codex" { + t.Fatalf("vendor = %s, want Codex", chatter.vendor.GetName()) + } + if chatter.model != "gpt-5.6-sol" { + t.Fatalf("model = %s, want gpt-5.6-sol", chatter.model) + } + }) + } +} + +func TestNewPluginRegistry_CodexTokenPersist(t *testing.T) { + dir := t.TempDir() + db := fsdb.NewDb(dir) + if err := db.SaveEnv("KEEP=old\n"); err != nil { + t.Fatalf("SaveEnv() error = %v", err) + } + + registry, err := NewPluginRegistry(db) + if err != nil { + t.Fatalf("NewPluginRegistry() error = %v", err) + } + + vendor := registry.VendorsAll.FindByName("Codex") + client, ok := vendor.(*codex.Client) + if !ok || client == nil { + t.Fatal("expected Codex client in VendorsAll") + } + if client.TokenPersist == nil { + t.Fatal("TokenPersist is nil") + } + + client.AccessToken.Value = "access-live" + client.RefreshToken.Value = "refresh-live" + client.AccountID.Value = "acct-live" + if err := client.TokenPersist(); err != nil { + t.Fatalf("TokenPersist() error = %v", err) + } + + parsed, err := godotenv.Read(db.EnvFilePath) + if err != nil { + t.Fatalf("godotenv.Read() error = %v", err) + } + if parsed["KEEP"] != "old" { + t.Fatalf("KEEP = %q, want old", parsed["KEEP"]) + } + if parsed[client.AccessToken.EnvVariable] != "access-live" { + t.Fatalf("access = %q, want access-live", parsed[client.AccessToken.EnvVariable]) + } + if parsed[client.RefreshToken.EnvVariable] != "refresh-live" { + t.Fatalf("refresh = %q, want refresh-live", parsed[client.RefreshToken.EnvVariable]) + } + if parsed[client.AccountID.EnvVariable] != "acct-live" { + t.Fatalf("account = %q, want acct-live", parsed[client.AccountID.EnvVariable]) + } +} + +func newInactiveVendorRegistry(t *testing.T, defaultVendor, defaultModel string, vendor *testVendor) *PluginRegistry { + t.Helper() + db := fsdb.NewDb(t.TempDir()) + all := ai.NewVendorsManager() + all.AddVendors(vendor) + defaults := &tools.Defaults{ + PluginBase: &plugins.PluginBase{}, + Vendor: &plugins.Setting{Value: defaultVendor}, + Model: &plugins.SetupQuestion{Setting: &plugins.Setting{Value: defaultModel}}, + ModelContextLength: &plugins.SetupQuestion{Setting: &plugins.Setting{Value: "0"}}, + } + return &PluginRegistry{ + Db: db, + VendorManager: ai.NewVendorsManager(), + VendorsAll: all, + Defaults: defaults, + } +} diff --git a/internal/i18n/locales/de.json b/internal/i18n/locales/de.json index b1b4bd9c..62e3b4ca 100644 --- a/internal/i18n/locales/de.json +++ b/internal/i18n/locales/de.json @@ -110,7 +110,9 @@ "choose_pattern_from_available": "Wähle ein Muster aus den verfügbaren Mustern", "choose_session_from_available": "Wähle eine Sitzung aus den verfügbaren Sitzungen", "choose_strategy_from_available": "Strategie aus den verfügbaren Strategien wählen", + "codex_api_base_url_question": "Geben Sie Ihre Codex-API-Basis-URL ein", "codex_auth_base_url_invalid": "Ungültige Codex-Authentifizierungs-Basis-URL: %w", + "codex_auth_base_url_question": "Geben Sie Ihre Codex-OAuth-Basis-URL ein", "codex_browser_open_fallback": "Falls Ihr Browser sich nicht geöffnet hat, navigieren Sie zu dieser URL zur Authentifizierung:", "codex_decode_models_response_failed": "Codex-Modell-Antwort konnte nicht dekodiert werden: %w", "codex_decode_refresh_response_failed": "Aktualisierte Codex-Token-Antwort konnte nicht dekodiert werden: %w", @@ -133,13 +135,16 @@ "codex_oauth_random_state_failed": "Sicherer zufälliger OAuth-Status konnte nicht generiert werden: %w", "codex_oauth_server_start_failed": "Lokaler OAuth-Callback-Server konnte nicht gestartet werden: %w", "codex_oauth_state_mismatch": "Status stimmt nicht überein", + "codex_provider_error": "Codex-Anbieterfehler (Status %d): %s", "codex_refresh_failed_status": "Codex-Anmeldung konnte nicht aktualisiert werden (Status %d)", "codex_refresh_login_failed": "Codex-Anmeldung konnte nicht aktualisiert werden: %w", "codex_refresh_token_required": "Codex-Aktualisierungstoken ist erforderlich. Bitte führen Sie 'fabric --setup' erneut aus.", "codex_replay_body_unavailable": "Anfragekörper kann für Codex-Reauthentifizierungswiederholung nicht wiedergegeben werden", + "codex_request_failed": "Codex-Anfrage fehlgeschlagen: %w", "codex_request_failed_status": "Codex-Anfrage fehlgeschlagen mit Status %d", "codex_starting_browser_login": "Starte browserbasierte OpenAI-Anmeldung für Codex.", "codex_token_exchange_failed": "Codex-Token-Austausch fehlgeschlagen: %w", + "codex_token_persist_failed": "Codex-Anmeldung konnte nicht gespeichert werden: %w", "codex_token_refresh_missing_access_token": "Die Codex-Token-Aktualisierung hat kein Zugriffstoken zurückgegeben.", "codex_usage_limit_reached": "Codex-Nutzungslimit erreicht", "command_completed_successfully": "Befehl erfolgreich abgeschlossen", @@ -168,6 +173,7 @@ "custom_patterns_setup_description": "Benutzerdefinierte Patterns - Verzeichnis für Ihre benutzerdefinierten Patterns festlegen", "custom_patterns_warning_create_directory": "Warnung: Benutzerdefiniertes Musterverzeichnis %s konnte nicht erstellt werden: %v\n", "db_error_loading_env_file": "fehler beim Laden der .env-Datei: %w", + "db_error_updating_env_file": "fehler beim Aktualisieren der .env-Datei: %w", "defaults_model_context_length_question": "Geben Sie die Kontextlänge des Modells ein", "defaults_model_question": "Geben Sie den Index oder den Namen Ihres Standardmodells ein", "defaults_setup_description": "Standard-KI-Anbieter und -Modell", diff --git a/internal/i18n/locales/en.json b/internal/i18n/locales/en.json index 893968dc..6429521d 100644 --- a/internal/i18n/locales/en.json +++ b/internal/i18n/locales/en.json @@ -110,7 +110,9 @@ "choose_pattern_from_available": "Choose a pattern from the available patterns", "choose_session_from_available": "Choose a session from the available sessions", "choose_strategy_from_available": "Choose a strategy from the available strategies", + "codex_api_base_url_question": "Enter your Codex API base URL", "codex_auth_base_url_invalid": "invalid codex auth base url: %w", + "codex_auth_base_url_question": "Enter your Codex OAuth base URL", "codex_browser_open_fallback": "If your browser did not open, navigate to this URL to authenticate:", "codex_decode_models_response_failed": "failed to decode codex models response: %w", "codex_decode_refresh_response_failed": "failed to decode refreshed Codex token response: %w", @@ -133,13 +135,16 @@ "codex_oauth_random_state_failed": "failed to generate secure random oauth state: %w", "codex_oauth_server_start_failed": "failed to start local oauth callback server: %w", "codex_oauth_state_mismatch": "State mismatch", + "codex_provider_error": "codex provider error (status %d): %s", "codex_refresh_failed_status": "failed to refresh codex login (status %d)", "codex_refresh_login_failed": "failed to refresh Codex login: %w", "codex_refresh_token_required": "Codex refresh token is required. Please rerun 'fabric --setup'.", "codex_replay_body_unavailable": "request body cannot be replayed for Codex re-authentication retry", + "codex_request_failed": "codex request failed: %w", "codex_request_failed_status": "codex request failed with status %d", "codex_starting_browser_login": "Starting browser-based OpenAI login for Codex.", "codex_token_exchange_failed": "codex token exchange failed: %w", + "codex_token_persist_failed": "failed to persist Codex login: %w", "codex_token_refresh_missing_access_token": "Codex token refresh did not return an access token.", "codex_usage_limit_reached": "codex usage limit reached", "command_completed_successfully": "Command completed successfully", @@ -168,6 +173,7 @@ "custom_patterns_setup_description": "Custom Patterns - Set directory for your custom patterns", "custom_patterns_warning_create_directory": "Warning: Could not create custom patterns directory %s: %v\n", "db_error_loading_env_file": "error loading .env file: %w", + "db_error_updating_env_file": "error updating .env file: %w", "defaults_model_context_length_question": "Enter model context length", "defaults_model_question": "Enter the index or the name of your default model", "defaults_setup_description": "Default AI Vendor and Model", diff --git a/internal/i18n/locales/es.json b/internal/i18n/locales/es.json index d538a876..2b544264 100644 --- a/internal/i18n/locales/es.json +++ b/internal/i18n/locales/es.json @@ -110,7 +110,9 @@ "choose_pattern_from_available": "Elige un patrón de los patrones disponibles", "choose_session_from_available": "Elige una sesión de las sesiones disponibles", "choose_strategy_from_available": "Elegir una estrategia de las estrategias disponibles", + "codex_api_base_url_question": "Ingrese la URL base de la API de Codex", "codex_auth_base_url_invalid": "URL base de autenticación de Codex no válida: %w", + "codex_auth_base_url_question": "Ingrese la URL base de OAuth de Codex", "codex_browser_open_fallback": "Si su navegador no se abrió, navegue a esta URL para autenticarse:", "codex_decode_models_response_failed": "No se pudo decodificar la respuesta de modelos de Codex: %w", "codex_decode_refresh_response_failed": "No se pudo decodificar la respuesta de token actualizado de Codex: %w", @@ -133,13 +135,16 @@ "codex_oauth_random_state_failed": "No se pudo generar un estado OAuth aleatorio seguro: %w", "codex_oauth_server_start_failed": "No se pudo iniciar el servidor local de callback OAuth: %w", "codex_oauth_state_mismatch": "El estado no coincide", + "codex_provider_error": "error del proveedor de Codex (estado %d): %s", "codex_refresh_failed_status": "No se pudo actualizar el inicio de sesión de Codex (estado %d)", "codex_refresh_login_failed": "No se pudo actualizar el inicio de sesión de Codex: %w", "codex_refresh_token_required": "Se requiere el token de actualización de Codex. Ejecute 'fabric --setup' de nuevo.", "codex_replay_body_unavailable": "El cuerpo de la solicitud no se puede reproducir para el reintento de reautenticación de Codex", + "codex_request_failed": "La solicitud de Codex falló: %w", "codex_request_failed_status": "La solicitud de Codex falló con estado %d", "codex_starting_browser_login": "Iniciando inicio de sesión de OpenAI basado en navegador para Codex.", "codex_token_exchange_failed": "El intercambio de token de Codex falló: %w", + "codex_token_persist_failed": "no se pudo guardar el inicio de sesión de Codex: %w", "codex_token_refresh_missing_access_token": "La actualización del token de Codex no devolvió un token de acceso.", "codex_usage_limit_reached": "Límite de uso de Codex alcanzado", "command_completed_successfully": "Comando completado exitosamente", @@ -168,6 +173,7 @@ "custom_patterns_setup_description": "Patrones personalizados - Establecer directorio para tus patrones personalizados", "custom_patterns_warning_create_directory": "Advertencia: No se pudo crear el directorio de patrones personalizados %s: %v\n", "db_error_loading_env_file": "error al cargar el archivo .env: %w", + "db_error_updating_env_file": "error al actualizar el archivo .env: %w", "defaults_model_context_length_question": "Introduce la longitud del contexto del modelo", "defaults_model_question": "Introduce el índice o el nombre de tu modelo predeterminado", "defaults_setup_description": "Proveedor y modelo de IA predeterminados", diff --git a/internal/i18n/locales/fa.json b/internal/i18n/locales/fa.json index 875c9cf3..7bb0eb8f 100644 --- a/internal/i18n/locales/fa.json +++ b/internal/i18n/locales/fa.json @@ -110,7 +110,9 @@ "choose_pattern_from_available": "الگویی از الگوهای موجود انتخاب کنید", "choose_session_from_available": "جلسه‌ای از جلسات موجود انتخاب کنید", "choose_strategy_from_available": "انتخاب استراتژی از استراتژی‌های موجود", + "codex_api_base_url_question": "آدرس پایه API Codex را وارد کنید", "codex_auth_base_url_invalid": "آدرس پایه احراز هویت Codex نامعتبر است: %w", + "codex_auth_base_url_question": "آدرس پایه OAuth Codex را وارد کنید", "codex_browser_open_fallback": "اگر مرورگر شما باز نشد، برای احراز هویت به این آدرس بروید:", "codex_decode_models_response_failed": "رمزگشایی پاسخ مدل‌های Codex ناموفق بود: %w", "codex_decode_refresh_response_failed": "رمزگشایی پاسخ توکن بازنشانی‌شده Codex ناموفق بود: %w", @@ -133,13 +135,16 @@ "codex_oauth_random_state_failed": "تولید وضعیت تصادفی امن OAuth ناموفق بود: %w", "codex_oauth_server_start_failed": "راه‌اندازی سرور محلی بازگشت OAuth ناموفق بود: %w", "codex_oauth_state_mismatch": "وضعیت مطابقت ندارد", + "codex_provider_error": "خطای ارائه‌دهنده Codex (وضعیت %d): %s", "codex_refresh_failed_status": "بازنشانی ورود Codex ناموفق بود (وضعیت %d)", "codex_refresh_login_failed": "بازنشانی ورود Codex ناموفق بود: %w", "codex_refresh_token_required": "توکن بازنشانی Codex مورد نیاز است. لطفاً 'fabric --setup' را دوباره اجرا کنید.", "codex_replay_body_unavailable": "بدنه درخواست برای تلاش مجدد احراز هویت Codex قابل بازپخش نیست", + "codex_request_failed": "درخواست Codex ناموفق بود: %w", "codex_request_failed_status": "درخواست Codex با وضعیت %d ناموفق بود", "codex_starting_browser_login": "شروع ورود مبتنی بر مرورگر OpenAI برای Codex.", "codex_token_exchange_failed": "تبادل توکن Codex ناموفق بود: %w", + "codex_token_persist_failed": "ذخیره ورود Codex ناموفق بود: %w", "codex_token_refresh_missing_access_token": "بازنشانی توکن Codex توکن دسترسی را برنگرداند.", "codex_usage_limit_reached": "محدودیت استفاده Codex به حداکثر رسیده است", "command_completed_successfully": "دستور با موفقیت تکمیل شد", @@ -168,6 +173,7 @@ "custom_patterns_setup_description": "الگوهای سفارشی - تنظیم دایرکتوری برای الگوهای سفارشی شما", "custom_patterns_warning_create_directory": "هشدار: امکان ایجاد پوشه الگوهای سفارشی %s وجود ندارد: %v\n", "db_error_loading_env_file": "خطا در بارگذاری فایل .env: %w", + "db_error_updating_env_file": "خطا در به‌روزرسانی فایل .env: %w", "defaults_model_context_length_question": "طول زمینه مدل را وارد کنید", "defaults_model_question": "شاخص یا نام مدل پیش‌فرض خود را وارد کنید", "defaults_setup_description": "ارائه‌دهنده و مدل هوش مصنوعی پیش‌فرض", diff --git a/internal/i18n/locales/fr.json b/internal/i18n/locales/fr.json index a9afe52d..d8b4f7f3 100644 --- a/internal/i18n/locales/fr.json +++ b/internal/i18n/locales/fr.json @@ -110,7 +110,9 @@ "choose_pattern_from_available": "Choisissez un motif parmi les motifs disponibles", "choose_session_from_available": "Choisissez une session parmi les sessions disponibles", "choose_strategy_from_available": "Choisir une stratégie parmi les stratégies disponibles", + "codex_api_base_url_question": "Entrez l'URL de base de l'API Codex", "codex_auth_base_url_invalid": "URL de base d'authentification Codex invalide : %w", + "codex_auth_base_url_question": "Entrez l'URL de base OAuth de Codex", "codex_browser_open_fallback": "Si votre navigateur ne s'est pas ouvert, accédez à cette URL pour vous authentifier :", "codex_decode_models_response_failed": "Échec du décodage de la réponse des modèles Codex : %w", "codex_decode_refresh_response_failed": "Échec du décodage de la réponse du jeton Codex rafraîchi : %w", @@ -133,13 +135,16 @@ "codex_oauth_random_state_failed": "Échec de la génération d'un état OAuth aléatoire sécurisé : %w", "codex_oauth_server_start_failed": "Échec du démarrage du serveur local de rappel OAuth : %w", "codex_oauth_state_mismatch": "L'état ne correspond pas", + "codex_provider_error": "erreur du fournisseur Codex (statut %d) : %s", "codex_refresh_failed_status": "Échec du rafraîchissement de la connexion Codex (statut %d)", "codex_refresh_login_failed": "Échec du rafraîchissement de la connexion Codex : %w", "codex_refresh_token_required": "Le jeton de rafraîchissement Codex est requis. Veuillez relancer 'fabric --setup'.", "codex_replay_body_unavailable": "Le corps de la requête ne peut pas être rejoué pour la tentative de réauthentification Codex", + "codex_request_failed": "La requête Codex a échoué : %w", "codex_request_failed_status": "La requête Codex a échoué avec le statut %d", "codex_starting_browser_login": "Démarrage de la connexion OpenAI par navigateur pour Codex.", "codex_token_exchange_failed": "L'échange de jeton Codex a échoué : %w", + "codex_token_persist_failed": "échec de l'enregistrement de la connexion Codex : %w", "codex_token_refresh_missing_access_token": "Le rafraîchissement du jeton Codex n'a pas renvoyé de jeton d'accès.", "codex_usage_limit_reached": "Limite d'utilisation Codex atteinte", "command_completed_successfully": "Commande terminée avec succès", @@ -168,6 +173,7 @@ "custom_patterns_setup_description": "Patrons personnalisés - Définir le répertoire pour vos patrons personnalisés", "custom_patterns_warning_create_directory": "Avertissement : Impossible de créer le répertoire de modèles personnalisés %s : %v\n", "db_error_loading_env_file": "erreur lors du chargement du fichier .env : %w", + "db_error_updating_env_file": "erreur lors de la mise à jour du fichier .env : %w", "defaults_model_context_length_question": "Saisissez la longueur du contexte du modèle", "defaults_model_question": "Saisissez l'index ou le nom de votre modèle par défaut", "defaults_setup_description": "Fournisseur et modèle d'IA par défaut", diff --git a/internal/i18n/locales/it.json b/internal/i18n/locales/it.json index c7fefb61..99be76a5 100644 --- a/internal/i18n/locales/it.json +++ b/internal/i18n/locales/it.json @@ -110,7 +110,9 @@ "choose_pattern_from_available": "Scegli un pattern dai pattern disponibili", "choose_session_from_available": "Scegli una sessione dalle sessioni disponibili", "choose_strategy_from_available": "Scegli una strategia dalle strategie disponibili", + "codex_api_base_url_question": "Inserisci l'URL di base dell'API Codex", "codex_auth_base_url_invalid": "URL base di autenticazione Codex non valido: %w", + "codex_auth_base_url_question": "Inserisci l'URL di base OAuth di Codex", "codex_browser_open_fallback": "Se il browser non si è aperto, navigare a questo URL per autenticarsi:", "codex_decode_models_response_failed": "Decodifica della risposta dei modelli Codex non riuscita: %w", "codex_decode_refresh_response_failed": "Decodifica della risposta del token Codex aggiornato non riuscita: %w", @@ -133,13 +135,16 @@ "codex_oauth_random_state_failed": "Generazione dello stato OAuth casuale sicuro non riuscita: %w", "codex_oauth_server_start_failed": "Avvio del server locale di callback OAuth non riuscito: %w", "codex_oauth_state_mismatch": "Lo stato non corrisponde", + "codex_provider_error": "errore del fornitore Codex (stato %d): %s", "codex_refresh_failed_status": "Aggiornamento dell'accesso Codex non riuscito (stato %d)", "codex_refresh_login_failed": "Aggiornamento dell'accesso Codex non riuscito: %w", "codex_refresh_token_required": "Il token di aggiornamento Codex è richiesto. Eseguire di nuovo 'fabric --setup'.", "codex_replay_body_unavailable": "Il corpo della richiesta non può essere riprodotto per il tentativo di riautenticazione Codex", + "codex_request_failed": "La richiesta Codex è fallita: %w", "codex_request_failed_status": "La richiesta Codex è fallita con stato %d", "codex_starting_browser_login": "Avvio dell'accesso OpenAI basato su browser per Codex.", "codex_token_exchange_failed": "Lo scambio di token Codex è fallito: %w", + "codex_token_persist_failed": "impossibile salvare l'accesso Codex: %w", "codex_token_refresh_missing_access_token": "L'aggiornamento del token Codex non ha restituito un token di accesso.", "codex_usage_limit_reached": "Limite di utilizzo Codex raggiunto", "command_completed_successfully": "Comando completato con successo", @@ -168,6 +173,7 @@ "custom_patterns_setup_description": "Pattern personalizzati - Imposta la directory per i tuoi pattern personalizzati", "custom_patterns_warning_create_directory": "Avviso: Impossibile creare la directory dei modelli personalizzati %s: %v\n", "db_error_loading_env_file": "errore nel caricamento del file .env: %w", + "db_error_updating_env_file": "errore nell'aggiornamento del file .env: %w", "defaults_model_context_length_question": "Inserisci la lunghezza del contesto del modello", "defaults_model_question": "Inserisci l'indice o il nome del tuo modello predefinito", "defaults_setup_description": "Fornitore e modello AI predefiniti", diff --git a/internal/i18n/locales/ja.json b/internal/i18n/locales/ja.json index cfe995c4..8c15a701 100644 --- a/internal/i18n/locales/ja.json +++ b/internal/i18n/locales/ja.json @@ -110,7 +110,9 @@ "choose_pattern_from_available": "利用可能なパターンからパターンを選択", "choose_session_from_available": "利用可能なセッションからセッションを選択", "choose_strategy_from_available": "利用可能な戦略から戦略を選択", + "codex_api_base_url_question": "Codex APIベースURLを入力してください", "codex_auth_base_url_invalid": "Codex認証ベースURLが無効です: %w", + "codex_auth_base_url_question": "Codex OAuthベースURLを入力してください", "codex_browser_open_fallback": "ブラウザが開かなかった場合は、このURLに移動して認証してください:", "codex_decode_models_response_failed": "Codexモデルレスポンスのデコードに失敗しました: %w", "codex_decode_refresh_response_failed": "更新されたCodexトークンレスポンスのデコードに失敗しました: %w", @@ -133,13 +135,16 @@ "codex_oauth_random_state_failed": "安全なランダムOAuthステートの生成に失敗しました: %w", "codex_oauth_server_start_failed": "ローカルOAuthコールバックサーバーの起動に失敗しました: %w", "codex_oauth_state_mismatch": "ステートが一致しません", + "codex_provider_error": "Codexベンダーエラー(ステータス %d): %s", "codex_refresh_failed_status": "Codexログインの更新に失敗しました(ステータス %d)", "codex_refresh_login_failed": "Codexログインの更新に失敗しました: %w", "codex_refresh_token_required": "Codexリフレッシュトークンが必要です。'fabric --setup'を再実行してください。", "codex_replay_body_unavailable": "Codex再認証リトライのためにリクエストボディを再送できません", + "codex_request_failed": "Codexリクエストに失敗しました: %w", "codex_request_failed_status": "Codexリクエストがステータス %d で失敗しました", "codex_starting_browser_login": "Codex用のブラウザベースOpenAIログインを開始しています。", "codex_token_exchange_failed": "Codexトークン交換に失敗しました: %w", + "codex_token_persist_failed": "Codexログインの保存に失敗しました: %w", "codex_token_refresh_missing_access_token": "Codexトークンの更新がアクセストークンを返しませんでした。", "codex_usage_limit_reached": "Codex使用量制限に達しました", "command_completed_successfully": "コマンドが正常に完了しました", @@ -168,6 +173,7 @@ "custom_patterns_setup_description": "カスタムパターン - カスタムパターン用のディレクトリを設定", "custom_patterns_warning_create_directory": "警告: カスタムパターンディレクトリ%sを作成できませんでした: %v\n", "db_error_loading_env_file": ".envファイルの読み込みエラー: %w", + "db_error_updating_env_file": ".envファイルの更新エラー: %w", "defaults_model_context_length_question": "モデルのコンテキスト長を入力してください", "defaults_model_question": "デフォルトモデルのインデックスまたは名前を入力してください", "defaults_setup_description": "デフォルトのAIプロバイダーとモデル", diff --git a/internal/i18n/locales/pl.json b/internal/i18n/locales/pl.json index 4992d77a..09f99ca9 100644 --- a/internal/i18n/locales/pl.json +++ b/internal/i18n/locales/pl.json @@ -110,7 +110,9 @@ "choose_pattern_from_available": "Wybierz wzorzec spośród dostępnych wzorców", "choose_session_from_available": "Wybierz sesję spośród dostępnych sesji", "choose_strategy_from_available": "Wybierz strategię spośród dostępnych strategii", + "codex_api_base_url_question": "Podaj bazowy URL API Codex", "codex_auth_base_url_invalid": "Nieprawidłowy bazowy URL uwierzytelniania Codex: %w", + "codex_auth_base_url_question": "Podaj bazowy URL OAuth Codex", "codex_browser_open_fallback": "Jeśli przeglądarka się nie otworzyła, przejdź pod ten URL, aby się uwierzytelnić:", "codex_decode_models_response_failed": "Nie udało się zdekodować odpowiedzi modeli Codex: %w", "codex_decode_refresh_response_failed": "Nie udało się zdekodować odpowiedzi odświeżonego tokenu Codex: %w", @@ -133,13 +135,16 @@ "codex_oauth_random_state_failed": "Nie udało się wygenerować bezpiecznego losowego stanu OAuth: %w", "codex_oauth_server_start_failed": "Nie udało się uruchomić lokalnego serwera zwrotnego OAuth: %w", "codex_oauth_state_mismatch": "Stan nie jest zgodny", + "codex_provider_error": "błąd dostawcy Codex (status %d): %s", "codex_refresh_failed_status": "Nie udało się odświeżyć logowania Codex (status %d)", "codex_refresh_login_failed": "Nie udało się odświeżyć logowania Codex: %w", "codex_refresh_token_required": "Token odświeżania Codex jest wymagany. Uruchom ponownie 'fabric --setup'.", "codex_replay_body_unavailable": "Treść żądania nie może być odtworzona dla ponownej próby uwierzytelnienia Codex", + "codex_request_failed": "Żądanie Codex nie powiodło się: %w", "codex_request_failed_status": "Żądanie Codex nie powiodło się ze statusem %d", "codex_starting_browser_login": "Uruchamianie logowania OpenAI przez przeglądarkę dla Codex.", "codex_token_exchange_failed": "Wymiana tokenu Codex nie powiodła się: %w", + "codex_token_persist_failed": "nie udało się zapisać logowania Codex: %w", "codex_token_refresh_missing_access_token": "Odświeżenie tokenu Codex nie zwróciło tokenu dostępu.", "codex_usage_limit_reached": "Osiągnięto limit użycia Codex", "command_completed_successfully": "Polecenie zakończone pomyślnie", @@ -168,6 +173,7 @@ "custom_patterns_setup_description": "Niestandardowe wzorce - Ustaw katalog dla swoich niestandardowych wzorców", "custom_patterns_warning_create_directory": "Ostrzeżenie: Nie można utworzyć katalogu niestandardowych wzorców %s: %v\n", "db_error_loading_env_file": "błąd podczas ładowania pliku .env: %w", + "db_error_updating_env_file": "błąd podczas aktualizacji pliku .env: %w", "defaults_model_context_length_question": "Podaj długość kontekstu modelu", "defaults_model_question": "Podaj indeks lub nazwę domyślnego modelu", "defaults_setup_description": "Domyślny dostawca AI i model", diff --git a/internal/i18n/locales/pt-BR.json b/internal/i18n/locales/pt-BR.json index b273efd2..5e308318 100644 --- a/internal/i18n/locales/pt-BR.json +++ b/internal/i18n/locales/pt-BR.json @@ -110,7 +110,9 @@ "choose_pattern_from_available": "Escolha um padrão entre os padrões disponíveis", "choose_session_from_available": "Escolha uma sessão das sessões disponíveis", "choose_strategy_from_available": "Escolher uma estratégia das estratégias disponíveis", + "codex_api_base_url_question": "Insira a URL base da API do Codex", "codex_auth_base_url_invalid": "URL base de autenticação do Codex inválida: %w", + "codex_auth_base_url_question": "Insira a URL base de OAuth do Codex", "codex_browser_open_fallback": "Se o navegador não abriu, navegue até esta URL para se autenticar:", "codex_decode_models_response_failed": "Falha ao decodificar a resposta de modelos do Codex: %w", "codex_decode_refresh_response_failed": "Falha ao decodificar a resposta do token atualizado do Codex: %w", @@ -133,13 +135,16 @@ "codex_oauth_random_state_failed": "Falha ao gerar estado OAuth aleatório seguro: %w", "codex_oauth_server_start_failed": "Falha ao iniciar o servidor local de callback OAuth: %w", "codex_oauth_state_mismatch": "O estado não corresponde", + "codex_provider_error": "erro do fornecedor Codex (status %d): %s", "codex_refresh_failed_status": "Falha ao atualizar o login do Codex (status %d)", "codex_refresh_login_failed": "Falha ao atualizar o login do Codex: %w", "codex_refresh_token_required": "O token de atualização do Codex é obrigatório. Execute 'fabric --setup' novamente.", "codex_replay_body_unavailable": "O corpo da requisição não pode ser reproduzido para a tentativa de reautenticação do Codex", + "codex_request_failed": "A requisição do Codex falhou: %w", "codex_request_failed_status": "A requisição do Codex falhou com status %d", "codex_starting_browser_login": "Iniciando login OpenAI baseado em navegador para o Codex.", "codex_token_exchange_failed": "A troca de token do Codex falhou: %w", + "codex_token_persist_failed": "falha ao persistir o login do Codex: %w", "codex_token_refresh_missing_access_token": "A atualização do token do Codex não retornou um token de acesso.", "codex_usage_limit_reached": "Limite de uso do Codex atingido", "command_completed_successfully": "Comando concluído com sucesso", @@ -168,6 +173,7 @@ "custom_patterns_setup_description": "Padrões personalizados - Definir diretório para seus padrões personalizados", "custom_patterns_warning_create_directory": "Aviso: Não foi possível criar o diretório de padrões personalizados %s: %v\n", "db_error_loading_env_file": "erro ao carregar o arquivo .env: %w", + "db_error_updating_env_file": "erro ao atualizar o arquivo .env: %w", "defaults_model_context_length_question": "Informe o comprimento do contexto do modelo", "defaults_model_question": "Informe o índice ou o nome do seu modelo padrão", "defaults_setup_description": "Provedor e modelo de IA padrão", diff --git a/internal/i18n/locales/pt-PT.json b/internal/i18n/locales/pt-PT.json index 93f2f339..a8587499 100644 --- a/internal/i18n/locales/pt-PT.json +++ b/internal/i18n/locales/pt-PT.json @@ -110,7 +110,9 @@ "choose_pattern_from_available": "Escolha um padrão dos padrões disponíveis", "choose_session_from_available": "Escolha uma sessão das sessões disponíveis", "choose_strategy_from_available": "Escolher uma estratégia das estratégias disponíveis", + "codex_api_base_url_question": "Introduza o URL base da API do Codex", "codex_auth_base_url_invalid": "URL base de autenticação do Codex inválido: %w", + "codex_auth_base_url_question": "Introduza o URL base de OAuth do Codex", "codex_browser_open_fallback": "Se o navegador não abriu, navegue até este URL para se autenticar:", "codex_decode_models_response_failed": "Falha ao descodificar a resposta de modelos do Codex: %w", "codex_decode_refresh_response_failed": "Falha ao descodificar a resposta do token atualizado do Codex: %w", @@ -133,13 +135,16 @@ "codex_oauth_random_state_failed": "Falha ao gerar estado OAuth aleatório seguro: %w", "codex_oauth_server_start_failed": "Falha ao iniciar o servidor local de retorno OAuth: %w", "codex_oauth_state_mismatch": "O estado não corresponde", + "codex_provider_error": "erro do fornecedor Codex (estado %d): %s", "codex_refresh_failed_status": "Falha ao atualizar o início de sessão do Codex (estado %d)", "codex_refresh_login_failed": "Falha ao atualizar o início de sessão do Codex: %w", "codex_refresh_token_required": "O token de atualização do Codex é obrigatório. Execute 'fabric --setup' novamente.", "codex_replay_body_unavailable": "O corpo do pedido não pode ser reproduzido para a tentativa de reautenticação do Codex", + "codex_request_failed": "O pedido do Codex falhou: %w", "codex_request_failed_status": "O pedido do Codex falhou com estado %d", "codex_starting_browser_login": "A iniciar início de sessão OpenAI baseado no navegador para o Codex.", "codex_token_exchange_failed": "A troca de token do Codex falhou: %w", + "codex_token_persist_failed": "falha ao persistir o início de sessão do Codex: %w", "codex_token_refresh_missing_access_token": "A atualização do token do Codex não devolveu um token de acesso.", "codex_usage_limit_reached": "Limite de utilização do Codex atingido", "command_completed_successfully": "Comando concluído com sucesso", @@ -168,6 +173,7 @@ "custom_patterns_setup_description": "Padrões personalizados - Definir diretório para os seus padrões personalizados", "custom_patterns_warning_create_directory": "Aviso: Não foi possível criar o diretório de padrões personalizados %s: %v\n", "db_error_loading_env_file": "erro ao carregar o ficheiro .env: %w", + "db_error_updating_env_file": "erro ao atualizar o ficheiro .env: %w", "defaults_model_context_length_question": "Indique o comprimento do contexto do modelo", "defaults_model_question": "Indique o índice ou o nome do seu modelo padrão", "defaults_setup_description": "Fornecedor e modelo de IA padrão", diff --git a/internal/i18n/locales/zh.json b/internal/i18n/locales/zh.json index 3b694a93..2f812ae8 100644 --- a/internal/i18n/locales/zh.json +++ b/internal/i18n/locales/zh.json @@ -110,7 +110,9 @@ "choose_pattern_from_available": "从可用模式中选择一个模式", "choose_session_from_available": "从可用会话中选择一个会话", "choose_strategy_from_available": "从可用策略中选择一个策略", + "codex_api_base_url_question": "请输入您的 Codex API 基础 URL", "codex_auth_base_url_invalid": "Codex 认证基础 URL 无效:%w", + "codex_auth_base_url_question": "请输入您的 Codex OAuth 基础 URL", "codex_browser_open_fallback": "如果浏览器未打开,请导航到此 URL 进行身份验证:", "codex_decode_models_response_failed": "解码 Codex 模型响应失败:%w", "codex_decode_refresh_response_failed": "解码刷新的 Codex 令牌响应失败:%w", @@ -133,13 +135,16 @@ "codex_oauth_random_state_failed": "生成安全随机 OAuth 状态失败:%w", "codex_oauth_server_start_failed": "启动本地 OAuth 回调服务器失败:%w", "codex_oauth_state_mismatch": "状态不匹配", + "codex_provider_error": "Codex 供应商错误(状态 %d):%s", "codex_refresh_failed_status": "刷新 Codex 登录失败(状态 %d)", "codex_refresh_login_failed": "刷新 Codex 登录失败:%w", "codex_refresh_token_required": "需要 Codex 刷新令牌。请重新运行 'fabric --setup'。", "codex_replay_body_unavailable": "请求正文无法重放用于 Codex 重新认证重试", + "codex_request_failed": "Codex 请求失败:%w", "codex_request_failed_status": "Codex 请求失败,状态 %d", "codex_starting_browser_login": "正在启动基于浏览器的 OpenAI 登录以连接 Codex。", "codex_token_exchange_failed": "Codex 令牌交换失败:%w", + "codex_token_persist_failed": "无法保存 Codex 登录:%w", "codex_token_refresh_missing_access_token": "Codex 令牌刷新未返回访问令牌。", "codex_usage_limit_reached": "已达到 Codex 使用限制", "command_completed_successfully": "命令执行成功", @@ -168,6 +173,7 @@ "custom_patterns_setup_description": "自定义模式 - 设置您的自定义模式目录", "custom_patterns_warning_create_directory": "警告:无法创建自定义模式目录 %s:%v\n", "db_error_loading_env_file": "加载 .env 文件错误:%w", + "db_error_updating_env_file": "更新 .env 文件错误:%w", "defaults_model_context_length_question": "请输入模型上下文长度", "defaults_model_question": "请输入您的默认模型的索引或名称", "defaults_setup_description": "默认 AI 提供商和模型", diff --git a/internal/plugins/ai/codex/auth_transport.go b/internal/plugins/ai/codex/auth_transport.go index 09daaa7d..29f2d0f5 100644 --- a/internal/plugins/ai/codex/auth_transport.go +++ b/internal/plugins/ai/codex/auth_transport.go @@ -24,6 +24,45 @@ type authTransport struct { } func (c *Client) ensureAccessToken(ctx context.Context, forceRefresh bool) (string, string, error) { + // Fast path: a valid in-memory token needs no refresh, so avoid the store + // lock and disk reload. This also keeps Setup's fresh OAuth tokens from + // being overwritten by the stale values still on disk before SaveEnvFile. + if !forceRefresh { + if access, account, ok := c.currentToken(); ok { + return access, account, nil + } + } + if c.WithStoreLock == nil { + return c.ensureAccessTokenLocked(ctx, forceRefresh) + } + var access, account string + err := c.WithStoreLock(func() error { + var inner error + access, account, inner = c.ensureAccessTokenLocked(ctx, forceRefresh) + return inner + }) + if err != nil { + return "", "", err + } + return access, account, nil +} + +// currentToken returns the in-memory token when it is present and unexpired. +// A missing account ID falls through to the locked path, which parses it from +// the JWT. +func (c *Client) currentToken() (access string, account string, ok bool) { + c.tokenMu.Lock() + defer c.tokenMu.Unlock() + + access = strings.TrimSpace(c.AccessToken.Value) + account = strings.TrimSpace(c.AccountID.Value) + if access == "" || account == "" || tokenNeedsRefresh(access, time.Now()) { + return "", "", false + } + return access, account, true +} + +func (c *Client) ensureAccessTokenLocked(ctx context.Context, forceRefresh bool) (string, string, error) { c.tokenMu.Lock() defer c.tokenMu.Unlock() @@ -61,7 +100,13 @@ func (c *Client) ensureAccessToken(ctx context.Context, forceRefresh bool) (stri c.setSettingValue(c.RefreshToken, refreshed.RefreshToken) } c.setSettingValue(c.AccountID, refreshedAccountID) - debuglog.Debug(debuglog.Detailed, "Codex access token refreshed for account=%s\n", refreshedAccountID) + if c.TokenPersist != nil { + if err := c.TokenPersist(); err != nil { + debuglog.Log("Codex token persist failed: %v\n", err) + return "", "", fmt.Errorf(i18n.T("codex_token_persist_failed"), err) + } + } + debuglog.Debug(debuglog.Detailed, "Codex access token refreshed account_present=%t\n", refreshedAccountID != "") return c.AccessToken.Value, c.AccountID.Value, nil } diff --git a/internal/plugins/ai/codex/codex.go b/internal/plugins/ai/codex/codex.go index 5ba59ff7..1deef7ed 100644 --- a/internal/plugins/ai/codex/codex.go +++ b/internal/plugins/ai/codex/codex.go @@ -58,6 +58,10 @@ type Client struct { apiHTTPClient *http.Client tokenMu sync.Mutex + // TokenPersist writes current tokens after refresh. Nil skips. Error fails the refresh. + TokenPersist func() error + // WithStoreLock, when set, wraps refresh+persist (reload tokens inside the lock). + WithStoreLock func(func() error) error } type modelInfo struct { @@ -81,11 +85,11 @@ func NewClient() *Client { client.AccountID = client.AddSetting("Account ID", true) client.ApiBaseURL = client.AddSetupQuestionWithEnvName("Base URL", false, - "Enter your Codex API base URL") + i18n.T("codex_api_base_url_question")) client.ApiBaseURL.Value = defaultBaseURL client.AuthBaseURL = client.AddSetupQuestionWithEnvName("Auth Base URL", false, - "Enter your Codex OAuth base URL") + i18n.T("codex_auth_base_url_question")) client.AuthBaseURL.Value = defaultAuthBaseURL client.authHTTPClient = &http.Client{Timeout: modelsRequestTimeout} @@ -101,6 +105,14 @@ func (c *Client) Setup() error { return err } + if strings.TrimSpace(c.RefreshToken.Value) != "" { + if err := c.configure(); err == nil { + return nil + } else { + debuglog.Log("Codex configure failed; starting browser login: %v\n", err) + } + } + ctx, cancel := context.WithTimeout(context.Background(), oauthTimeout) defer cancel() @@ -153,11 +165,26 @@ func (c *Client) configure() error { option.WithHTTPClient(c.apiHTTPClient), ) c.ApiClient = &apiClient - debuglog.Debug(debuglog.Detailed, "Codex configure: authenticated account=%s base_url=%s\n", c.AccountID.Value, c.ApiBaseURL.Value) + debuglog.Debug(debuglog.Detailed, "Codex configure: authenticated account_present=%t base_url=%s\n", strings.TrimSpace(c.AccountID.Value) != "", c.ApiBaseURL.Value) return nil } +// LoadEnvSettings copies non-empty Codex token values from env. +func (c *Client) LoadEnvSettings(env map[string]string) { + apply := func(setting *plugins.Setting) { + if setting == nil || setting.EnvVariable == "" { + return + } + if value := strings.TrimSpace(env[setting.EnvVariable]); value != "" { + c.setSettingValue(setting, value) + } + } + apply(c.AccessToken) + apply(c.RefreshToken) + apply(c.AccountID) +} + // ListModels returns the Codex models available to the configured account. func (c *Client) ListModels(ctx context.Context) ([]string, error) { if c.apiHTTPClient == nil { diff --git a/internal/plugins/ai/codex/codex_test.go b/internal/plugins/ai/codex/codex_test.go index 92430b56..209dfdbd 100644 --- a/internal/plugins/ai/codex/codex_test.go +++ b/internal/plugins/ai/codex/codex_test.go @@ -19,6 +19,7 @@ import ( "github.com/danielmiessler/fabric/internal/chat" "github.com/danielmiessler/fabric/internal/domain" + "github.com/danielmiessler/fabric/internal/i18n" openaiapi "github.com/openai/openai-go" "github.com/openai/openai-go/shared/constant" ) @@ -211,6 +212,10 @@ func TestNormalizeSemverLikeVersion(t *testing.T) { } func TestMapRequestErrorPreservesCodexAPIErrorMessage(t *testing.T) { + if _, err := i18n.Init("en"); err != nil { + t.Fatalf("i18n.Init() error = %v", err) + } + client := NewClient() apiErr := &openaiapi.Error{StatusCode: http.StatusBadRequest} if err := apiErr.UnmarshalJSON([]byte(`{"message":"The requested model is not supported.","type":"invalid_request_error","param":"model","code":"invalid_value"}`)); err != nil { @@ -221,8 +226,9 @@ func TestMapRequestErrorPreservesCodexAPIErrorMessage(t *testing.T) { if err == nil { t.Fatal("mapRequestError() returned nil") } - if got := err.Error(); got != "codex request failed with status 400" { - t.Fatalf("mapRequestError() = %q, want %q", got, "codex request failed with status 400") + want := fmt.Sprintf(i18n.T("codex_request_failed_status"), http.StatusBadRequest) + if got := err.Error(); got != want { + t.Fatalf("mapRequestError() = %q, want %q", got, want) } if unwrapped := errors.Unwrap(err); unwrapped == nil || !strings.Contains(unwrapped.Error(), "The requested model is not supported.") { t.Fatalf("wrapped error = %v, want provider detail", unwrapped) @@ -230,6 +236,10 @@ func TestMapRequestErrorPreservesCodexAPIErrorMessage(t *testing.T) { } func TestMapRequestErrorReadsAPIErrorResponseBodyWhenRawJSONMissing(t *testing.T) { + if _, err := i18n.Init("en"); err != nil { + t.Fatalf("i18n.Init() error = %v", err) + } + client := NewClient() apiErr := &openaiapi.Error{ StatusCode: http.StatusBadRequest, @@ -242,8 +252,9 @@ func TestMapRequestErrorReadsAPIErrorResponseBodyWhenRawJSONMissing(t *testing.T if err == nil { t.Fatal("mapRequestError() returned nil") } - if got := err.Error(); got != "codex request failed with status 400" { - t.Fatalf("mapRequestError() = %q, want %q", got, "codex request failed with status 400") + want := fmt.Sprintf(i18n.T("codex_request_failed_status"), http.StatusBadRequest) + if got := err.Error(); got != want { + t.Fatalf("mapRequestError() = %q, want %q", got, want) } if unwrapped := errors.Unwrap(err); unwrapped == nil || !strings.Contains(unwrapped.Error(), "The requested model is not supported for Codex.") { t.Fatalf("wrapped error = %v, want provider detail", unwrapped) @@ -575,8 +586,8 @@ func TestSendStreamClosesChannelAndMapsHTTPError(t *testing.T) { if err == nil { t.Fatal("SendStream() error = nil, want mapped HTTP error") } - if got := err.Error(); got != "codex usage limit reached" { - t.Fatalf("SendStream() error = %q, want %q", got, "codex usage limit reached") + if got := err.Error(); got != i18n.T("codex_usage_limit_reached") { + t.Fatalf("SendStream() error = %q, want %q", got, i18n.T("codex_usage_limit_reached")) } update, ok := <-updates @@ -585,6 +596,159 @@ func TestSendStreamClosesChannelAndMapsHTTPError(t *testing.T) { } } +func TestEnsureAccessTokenPersistsRotatedRefreshToken(t *testing.T) { + authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/oauth/token" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(refreshResponse{ + IDToken: testJWT("acct_persist", time.Now().Add(2*time.Hour)), + AccessToken: testJWT("acct_persist", time.Now().Add(2*time.Hour)), + RefreshToken: "refresh-rotated", + }) + })) + defer authServer.Close() + + var persisted map[string]string + client := NewClient() + client.ApiBaseURL.Value = "https://example.invalid" + client.AuthBaseURL.Value = authServer.URL + client.RefreshToken.Value = "refresh-old" + client.AccessToken.Value = testJWT("acct_persist", time.Now().Add(-time.Hour)) + client.AccountID.Value = "acct_persist" + client.TokenPersist = func() error { + persisted = map[string]string{ + "access": client.AccessToken.Value, + "refresh": client.RefreshToken.Value, + "account": client.AccountID.Value, + } + return nil + } + + access, account, err := client.ensureAccessToken(context.Background(), false) + if err != nil { + t.Fatalf("ensureAccessToken() error = %v", err) + } + if access == "" { + t.Fatal("ensureAccessToken() returned empty access token") + } + if account != "acct_persist" { + t.Fatalf("account = %q, want acct_persist", account) + } + if persisted["refresh"] != "refresh-rotated" { + t.Fatalf("persisted refresh = %q, want refresh-rotated", persisted["refresh"]) + } +} + +func TestEnsureAccessTokenPersistError(t *testing.T) { + if _, err := i18n.Init("en"); err != nil { + t.Fatalf("i18n.Init() error = %v", err) + } + + authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/oauth/token" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(refreshResponse{ + IDToken: testJWT("acct_persist", time.Now().Add(2*time.Hour)), + AccessToken: testJWT("acct_persist", time.Now().Add(2*time.Hour)), + RefreshToken: "refresh-rotated", + }) + })) + defer authServer.Close() + + persistErr := errors.New("disk full") + client := NewClient() + client.ApiBaseURL.Value = "https://example.invalid" + client.AuthBaseURL.Value = authServer.URL + client.RefreshToken.Value = "refresh-old" + client.AccessToken.Value = testJWT("acct_persist", time.Now().Add(-time.Hour)) + client.AccountID.Value = "acct_persist" + client.TokenPersist = func() error { + return persistErr + } + + _, _, err := client.ensureAccessToken(context.Background(), false) + if err == nil { + t.Fatal("ensureAccessToken() error = nil, want persist error") + } + if !errors.Is(err, persistErr) { + t.Fatalf("ensureAccessToken() error = %v, want persist error %v", err, persistErr) + } +} + +func TestEnsureAccessTokenReloadsStoreBeforeRefresh(t *testing.T) { + authHits := 0 + authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHits++ + http.Error(w, "unexpected refresh", http.StatusInternalServerError) + })) + defer authServer.Close() + + fresh := testJWT("acct_reload", time.Now().Add(2*time.Hour)) + client := NewClient() + client.ApiBaseURL.Value = "https://example.invalid" + client.AuthBaseURL.Value = authServer.URL + client.RefreshToken.Value = "refresh-old" + client.AccessToken.Value = testJWT("acct_reload", time.Now().Add(-time.Hour)) + client.AccountID.Value = "acct_reload" + client.WithStoreLock = func(fn func() error) error { + client.AccessToken.Value = fresh + client.AccountID.Value = "acct_reload" + return fn() + } + + access, account, err := client.ensureAccessToken(context.Background(), false) + if err != nil { + t.Fatalf("ensureAccessToken() error = %v", err) + } + if access != fresh { + t.Fatal("ensureAccessToken() did not use reloaded access token") + } + if account != "acct_reload" { + t.Fatalf("account = %q, want acct_reload", account) + } + if authHits != 0 { + t.Fatalf("auth server hits = %d, want 0", authHits) + } +} + +// Regression: a valid in-memory token (e.g. just obtained via Setup's OAuth) +// must not be reloaded from the store, which still holds the stale token until +// SaveEnvFile runs. The fast path returns before WithStoreLock can clobber it. +func TestEnsureAccessTokenKeepsFreshTokenOverStore(t *testing.T) { + fresh := testJWT("acct_fresh", time.Now().Add(2*time.Hour)) + client := NewClient() + client.ApiBaseURL.Value = "https://example.invalid" + client.AuthBaseURL.Value = "https://auth.invalid" + client.RefreshToken.Value = "refresh-new" + client.AccessToken.Value = fresh + client.AccountID.Value = "acct_fresh" + + storeLocked := false + client.WithStoreLock = func(fn func() error) error { + storeLocked = true + client.AccessToken.Value = "stale-from-disk" + client.AccountID.Value = "acct_stale" + return fn() + } + + access, account, err := client.ensureAccessToken(context.Background(), false) + if err != nil { + t.Fatalf("ensureAccessToken() error = %v", err) + } + if storeLocked { + t.Fatal("store lock ran and clobbered a valid in-memory token") + } + if access != fresh || account != "acct_fresh" { + t.Fatalf("got access=%q account=%q, want fresh token for acct_fresh", access, account) + } +} + func newConfiguredTestClient(t *testing.T, apiBaseURL string, accountID string, accessToken string) *Client { t.Helper() diff --git a/internal/plugins/ai/codex/errors.go b/internal/plugins/ai/codex/errors.go index 12d2dd14..af8cc2bb 100644 --- a/internal/plugins/ai/codex/errors.go +++ b/internal/plugins/ai/codex/errors.go @@ -77,7 +77,7 @@ func (c *Client) mapRequestError(err error) error { case isUsageLimitMessage(message): return &publicError{ message: i18n.T("codex_usage_limit_reached"), - cause: fmt.Errorf("codex request failed: %w", err), + cause: fmt.Errorf(i18n.T("codex_request_failed"), err), } default: return err @@ -91,7 +91,7 @@ func wrapPublicError(message string, statusCode int, providerMessage string) err return &publicError{ message: message, - cause: fmt.Errorf("codex provider error (status %d): %s", statusCode, providerMessage), + cause: fmt.Errorf(i18n.T("codex_provider_error"), statusCode, providerMessage), } } diff --git a/internal/plugins/db/fsdb/db.go b/internal/plugins/db/fsdb/db.go index e3c6dea1..4b54d17b 100644 --- a/internal/plugins/db/fsdb/db.go +++ b/internal/plugins/db/fsdb/db.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strings" + "sync" "time" "github.com/danielmiessler/fabric/internal/i18n" @@ -41,6 +42,8 @@ type Db struct { Contexts *ContextsEntity EnvFilePath string + + envMu sync.Mutex } func (o *Db) Configure() (err error) { @@ -92,9 +95,100 @@ func (o *Db) IsEnvFileExists() (ret bool) { return } -func (o *Db) SaveEnv(content string) (err error) { - err = os.WriteFile(o.EnvFilePath, []byte(content), 0644) - return +func (o *Db) SaveEnv(content string) error { + return o.WithEnvLock(func() error { + if err := writeFileAtomic(o.EnvFilePath, []byte(content)); err != nil { + return fmt.Errorf(i18n.T("db_error_updating_env_file"), err) + } + return nil + }) +} + +func (o *Db) ReadEnvFile() (map[string]string, error) { + env, err := godotenv.Read(o.EnvFilePath) + if err != nil { + if o.IsEnvFileExists() { + return nil, fmt.Errorf(i18n.T("db_error_loading_env_file"), err) + } + return map[string]string{}, nil + } + return env, nil +} + +func (o *Db) WithEnvLock(fn func() error) error { + o.envMu.Lock() + defer o.envMu.Unlock() + + lockFile, err := os.OpenFile(o.EnvFilePath+".lock", os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + return fmt.Errorf(i18n.T("db_error_updating_env_file"), err) + } + defer lockFile.Close() + if err := lockExclusive(lockFile); err != nil { + return fmt.Errorf(i18n.T("db_error_updating_env_file"), err) + } + defer unlockExclusive(lockFile) + + return fn() +} + +// UpdateEnvVars merges non-empty values into .env under a file lock. +// Comments and key order are not preserved. +func (o *Db) UpdateEnvVars(updates map[string]string) error { + return o.WithEnvLock(func() error { + return o.ApplyEnvUpdates(updates) + }) +} + +// ApplyEnvUpdates writes non-empty updates atomically. Callers holding WithEnvLock use this. +func (o *Db) ApplyEnvUpdates(updates map[string]string) error { + env, err := o.ReadEnvFile() + if err != nil { + return err + } + for key, value := range updates { + if strings.TrimSpace(value) == "" { + continue + } + env[key] = value + } + if err := writeEnvFileAtomic(o.EnvFilePath, env); err != nil { + return fmt.Errorf(i18n.T("db_error_updating_env_file"), err) + } + return nil +} + +func writeEnvFileAtomic(path string, env map[string]string) error { + content, err := godotenv.Marshal(env) + if err != nil { + return err + } + return writeFileAtomic(path, []byte(content+"\n")) +} + +func writeFileAtomic(path string, content []byte) error { + tmp, err := os.CreateTemp(filepath.Dir(path), ".env.tmp-") + if err != nil { + return err + } + tmpName := tmp.Name() + defer os.Remove(tmpName) + + if err := tmp.Chmod(0600); err != nil { + _ = tmp.Close() + return err + } + _, err = tmp.Write(content) + if err == nil { + err = tmp.Sync() + } + if cerr := tmp.Close(); err == nil { + err = cerr + } + if err != nil { + return err + } + return os.Rename(tmpName, path) } func (o *Db) FilePath(fileName string) (ret string) { diff --git a/internal/plugins/db/fsdb/db_test.go b/internal/plugins/db/fsdb/db_test.go index 3971d1ee..ecedaeb9 100644 --- a/internal/plugins/db/fsdb/db_test.go +++ b/internal/plugins/db/fsdb/db_test.go @@ -2,7 +2,12 @@ package fsdb import ( "os" + "path/filepath" + "runtime" + "sync" "testing" + + "github.com/joho/godotenv" ) func TestDb_Configure(t *testing.T) { @@ -52,4 +57,150 @@ func TestDb_SaveEnv(t *testing.T) { if _, err := os.Stat(db.EnvFilePath); os.IsNotExist(err) { t.Errorf("expected .env file to be saved") } + assertEnvMode(t, db.EnvFilePath) +} + +func TestDb_UpdateEnvVars(t *testing.T) { + dir := t.TempDir() + db := NewDb(dir) + if err := db.SaveEnv("KEEP=old\nCODEX_REFRESH_TOKEN=stale\n"); err != nil { + t.Fatalf("SaveEnv() error = %v", err) + } + + if err := db.UpdateEnvVars(map[string]string{ + "CODEX_REFRESH_TOKEN": "rotated", + "CODEX_ACCESS_TOKEN": "fresh", + }); err != nil { + t.Fatalf("UpdateEnvVars() error = %v", err) + } + + parsed, err := godotenv.Read(db.EnvFilePath) + if err != nil { + t.Fatalf("godotenv.Read() error = %v", err) + } + if parsed["KEEP"] != "old" { + t.Fatalf("KEEP = %q, want old", parsed["KEEP"]) + } + if parsed["CODEX_REFRESH_TOKEN"] != "rotated" { + t.Fatalf("CODEX_REFRESH_TOKEN = %q, want rotated", parsed["CODEX_REFRESH_TOKEN"]) + } + if parsed["CODEX_ACCESS_TOKEN"] != "fresh" { + t.Fatalf("CODEX_ACCESS_TOKEN = %q, want fresh", parsed["CODEX_ACCESS_TOKEN"]) + } + assertEnvMode(t, db.EnvFilePath) +} + +func TestDb_UpdateEnvVars_MissingFile(t *testing.T) { + dir := t.TempDir() + db := NewDb(dir) + + if err := db.UpdateEnvVars(map[string]string{"CODEX_ACCESS_TOKEN": "fresh"}); err != nil { + t.Fatalf("UpdateEnvVars() error = %v", err) + } + + parsed, err := godotenv.Read(db.EnvFilePath) + if err != nil { + t.Fatalf("godotenv.Read() error = %v", err) + } + if parsed["CODEX_ACCESS_TOKEN"] != "fresh" { + t.Fatalf("CODEX_ACCESS_TOKEN = %q, want fresh", parsed["CODEX_ACCESS_TOKEN"]) + } + assertEnvMode(t, db.EnvFilePath) +} + +func TestDb_UpdateEnvVars_CorruptFile(t *testing.T) { + dir := t.TempDir() + db := NewDb(dir) + if err := os.Mkdir(db.EnvFilePath, 0700); err != nil { + t.Fatalf("Mkdir() error = %v", err) + } + + err := db.UpdateEnvVars(map[string]string{"CODEX_ACCESS_TOKEN": "fresh"}) + if err == nil { + t.Fatal("UpdateEnvVars() error = nil, want corrupt-file error") + } +} + +func TestDb_UpdateEnvVars_SkipEmpty(t *testing.T) { + dir := t.TempDir() + db := NewDb(dir) + if err := db.SaveEnv("CODEX_REFRESH_TOKEN=live\nKEEP=old\n"); err != nil { + t.Fatalf("SaveEnv() error = %v", err) + } + + if err := db.UpdateEnvVars(map[string]string{ + "CODEX_REFRESH_TOKEN": "", + "CODEX_ACCESS_TOKEN": "fresh", + }); err != nil { + t.Fatalf("UpdateEnvVars() error = %v", err) + } + + parsed, err := godotenv.Read(db.EnvFilePath) + if err != nil { + t.Fatalf("godotenv.Read() error = %v", err) + } + if parsed["CODEX_REFRESH_TOKEN"] != "live" { + t.Fatalf("CODEX_REFRESH_TOKEN = %q, want live", parsed["CODEX_REFRESH_TOKEN"]) + } + if parsed["CODEX_ACCESS_TOKEN"] != "fresh" { + t.Fatalf("CODEX_ACCESS_TOKEN = %q, want fresh", parsed["CODEX_ACCESS_TOKEN"]) + } + if parsed["KEEP"] != "old" { + t.Fatalf("KEEP = %q, want old", parsed["KEEP"]) + } +} + +func TestDb_UpdateEnvVars_Concurrent(t *testing.T) { + dir := t.TempDir() + db := NewDb(dir) + if err := db.SaveEnv("KEEP=old\n"); err != nil { + t.Fatalf("SaveEnv() error = %v", err) + } + + var wg sync.WaitGroup + errs := make(chan error, 2) + wg.Add(2) + go func() { + defer wg.Done() + errs <- db.UpdateEnvVars(map[string]string{"CODEX_ACCESS_TOKEN": "one"}) + }() + go func() { + defer wg.Done() + errs <- db.UpdateEnvVars(map[string]string{"CODEX_REFRESH_TOKEN": "two"}) + }() + wg.Wait() + close(errs) + for err := range errs { + if err != nil { + t.Fatalf("UpdateEnvVars() error = %v", err) + } + } + + parsed, err := godotenv.Read(db.EnvFilePath) + if err != nil { + t.Fatalf("godotenv.Read() error = %v", err) + } + if parsed["KEEP"] != "old" { + t.Fatalf("KEEP = %q, want old", parsed["KEEP"]) + } + if parsed["CODEX_ACCESS_TOKEN"] != "one" { + t.Fatalf("CODEX_ACCESS_TOKEN = %q, want one", parsed["CODEX_ACCESS_TOKEN"]) + } + if parsed["CODEX_REFRESH_TOKEN"] != "two" { + t.Fatalf("CODEX_REFRESH_TOKEN = %q, want two", parsed["CODEX_REFRESH_TOKEN"]) + } +} + +func assertEnvMode(t *testing.T, path string) { + t.Helper() + if runtime.GOOS == "windows" { + return + } + info, err := os.Stat(path) + if err != nil { + t.Fatalf("Stat(%s) error = %v", path, err) + } + if perm := info.Mode().Perm(); perm != 0600 { + t.Fatalf("%s mode = %o, want 0600", filepath.Base(path), perm) + } } diff --git a/internal/plugins/db/fsdb/env_lock_unix.go b/internal/plugins/db/fsdb/env_lock_unix.go new file mode 100644 index 00000000..09ed2dac --- /dev/null +++ b/internal/plugins/db/fsdb/env_lock_unix.go @@ -0,0 +1,22 @@ +//go:build unix + +package fsdb + +import ( + "os" + "runtime" + + "golang.org/x/sys/unix" +) + +func lockExclusive(f *os.File) error { + err := unix.Flock(int(f.Fd()), unix.LOCK_EX) + runtime.KeepAlive(f) + return err +} + +func unlockExclusive(f *os.File) error { + err := unix.Flock(int(f.Fd()), unix.LOCK_UN) + runtime.KeepAlive(f) + return err +} diff --git a/internal/plugins/db/fsdb/env_lock_windows.go b/internal/plugins/db/fsdb/env_lock_windows.go new file mode 100644 index 00000000..7d391f59 --- /dev/null +++ b/internal/plugins/db/fsdb/env_lock_windows.go @@ -0,0 +1,19 @@ +//go:build windows + +package fsdb + +import ( + "os" + + "golang.org/x/sys/windows" +) + +func lockExclusive(f *os.File) error { + var overlapped windows.Overlapped + return windows.LockFileEx(windows.Handle(f.Fd()), windows.LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &overlapped) +} + +func unlockExclusive(f *os.File) error { + var overlapped windows.Overlapped + return windows.UnlockFileEx(windows.Handle(f.Fd()), 0, 1, 0, &overlapped) +}